diff --git a/.github/golangci.yml b/.github/golangci.yml index 43cea27a791..5be4fb5fdf4 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -79,6 +79,7 @@ issues: - gosec # Disabled linting of weak number generators - makezero # Disabled linting of intentional slice appends - goconst # Disabled linting of common mnemonics and test case strings + - unused # Disabled linting of unused mock methods - path: _\.gno linters: - errorlint # Disabled linting of error comparisons, because of lacking std lib support diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index a315d88591c..310da4e0df6 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -80,6 +80,7 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.5.4 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e38c3621483..96b11146f85 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -221,6 +221,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index c5bb1ad0d81..a1f80a1c08e 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -32,6 +32,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index f4bdc65d7ec..76cbdc05ed0 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -113,6 +113,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index 393fed0725d..61f210c80fd 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -33,6 +33,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect @@ -50,6 +51,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index f3161e47bad..82fe1918f59 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -122,6 +122,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index e6d9f119c7b..4db099fb330 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -22,6 +22,7 @@ require ( github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect @@ -35,6 +36,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index 116cfbff021..da4d38a59fc 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -105,6 +105,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -151,6 +153,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 0c794afd54c..f5ea9d107b7 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -36,6 +36,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -53,6 +54,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 50eb5add218..16fe8a08090 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -126,6 +126,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -182,6 +184,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index c492ae7c818..44d2e92c764 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -30,6 +30,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.opentelemetry.io/otel v1.29.0 // indirect @@ -45,6 +46,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index f3161e47bad..82fe1918f59 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -122,6 +122,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/gno.land/cmd/gnoland/config_get_test.go b/gno.land/cmd/gnoland/config_get_test.go index f2ddc5ca6d0..84cf0ba3d37 100644 --- a/gno.land/cmd/gnoland/config_get_test.go +++ b/gno.land/cmd/gnoland/config_get_test.go @@ -289,14 +289,6 @@ func TestConfig_Get_Base(t *testing.T) { }, true, }, - { - "filter peers flag fetched", - "filter_peers", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.FilterPeers, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, } verifyGetTestTableCommon(t, testTable) @@ -616,19 +608,11 @@ func TestConfig_Get_P2P(t *testing.T) { }, true, }, - { - "upnp toggle", - "p2p.upnp", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.UPNP, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, { "max inbound peers", "p2p.max_num_inbound_peers", func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.MaxNumInboundPeers, unmarshalJSONCommon[int](t, value)) + assert.Equal(t, loadedCfg.P2P.MaxNumInboundPeers, unmarshalJSONCommon[uint64](t, value)) }, false, }, @@ -636,7 +620,7 @@ func TestConfig_Get_P2P(t *testing.T) { "max outbound peers", "p2p.max_num_outbound_peers", func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.MaxNumOutboundPeers, unmarshalJSONCommon[int](t, value)) + assert.Equal(t, loadedCfg.P2P.MaxNumOutboundPeers, unmarshalJSONCommon[uint64](t, value)) }, false, }, @@ -676,15 +660,7 @@ func TestConfig_Get_P2P(t *testing.T) { "pex reactor toggle", "p2p.pex", func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.PexReactor, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, - { - "seed mode", - "p2p.seed_mode", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.SeedMode, unmarshalJSONCommon[bool](t, value)) + assert.Equal(t, loadedCfg.P2P.PeerExchange, unmarshalJSONCommon[bool](t, value)) }, false, }, @@ -704,30 +680,6 @@ func TestConfig_Get_P2P(t *testing.T) { }, true, }, - { - "allow duplicate IP", - "p2p.allow_duplicate_ip", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.AllowDuplicateIP, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, - { - "handshake timeout", - "p2p.handshake_timeout", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.HandshakeTimeout, unmarshalJSONCommon[time.Duration](t, value)) - }, - false, - }, - { - "dial timeout", - "p2p.dial_timeout", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.DialTimeout, unmarshalJSONCommon[time.Duration](t, value)) - }, - false, - }, } verifyGetTestTableCommon(t, testTable) diff --git a/gno.land/cmd/gnoland/config_set_test.go b/gno.land/cmd/gnoland/config_set_test.go index cb831f0e502..39880313043 100644 --- a/gno.land/cmd/gnoland/config_set_test.go +++ b/gno.land/cmd/gnoland/config_set_test.go @@ -244,19 +244,6 @@ func TestConfig_Set_Base(t *testing.T) { assert.Equal(t, value, loadedCfg.ProfListenAddress) }, }, - { - "filter peers flag updated", - []string{ - "filter_peers", - "true", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.FilterPeers) - }, - }, } verifySetTestTableCommon(t, testTable) @@ -505,19 +492,6 @@ func TestConfig_Set_P2P(t *testing.T) { assert.Equal(t, value, loadedCfg.P2P.PersistentPeers) }, }, - { - "upnp toggle updated", - []string{ - "p2p.upnp", - "false", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.P2P.UPNP) - }, - }, { "max inbound peers updated", []string{ @@ -588,20 +562,7 @@ func TestConfig_Set_P2P(t *testing.T) { boolVal, err := strconv.ParseBool(value) require.NoError(t, err) - assert.Equal(t, boolVal, loadedCfg.P2P.PexReactor) - }, - }, - { - "seed mode updated", - []string{ - "p2p.seed_mode", - "false", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.P2P.SeedMode) + assert.Equal(t, boolVal, loadedCfg.P2P.PeerExchange) }, }, { @@ -614,39 +575,6 @@ func TestConfig_Set_P2P(t *testing.T) { assert.Equal(t, value, loadedCfg.P2P.PrivatePeerIDs) }, }, - { - "allow duplicate IPs updated", - []string{ - "p2p.allow_duplicate_ip", - "false", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.P2P.AllowDuplicateIP) - }, - }, - { - "handshake timeout updated", - []string{ - "p2p.handshake_timeout", - "1s", - }, - func(loadedCfg *config.Config, value string) { - assert.Equal(t, value, loadedCfg.P2P.HandshakeTimeout.String()) - }, - }, - { - "dial timeout updated", - []string{ - "p2p.dial_timeout", - "1s", - }, - func(loadedCfg *config.Config, value string) { - assert.Equal(t, value, loadedCfg.P2P.DialTimeout.String()) - }, - }, } verifySetTestTableCommon(t, testTable) diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go index d40e90f6b48..500336e3489 100644 --- a/gno.land/cmd/gnoland/secrets_common.go +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) var ( @@ -54,7 +54,7 @@ func isValidDirectory(dirPath string) bool { } type secretData interface { - privval.FilePVKey | privval.FilePVLastSignState | p2p.NodeKey + privval.FilePVKey | privval.FilePVLastSignState | types.NodeKey } // readSecretData reads the secret data from the given path @@ -145,7 +145,7 @@ func validateValidatorStateSignature( } // validateNodeKey validates the node's p2p key -func validateNodeKey(key *p2p.NodeKey) error { +func validateNodeKey(key *types.NodeKey) error { if key.PrivKey == nil { return errInvalidNodeKey } diff --git a/gno.land/cmd/gnoland/secrets_common_test.go b/gno.land/cmd/gnoland/secrets_common_test.go index 34592c3bd8f..38c4772c705 100644 --- a/gno.land/cmd/gnoland/secrets_common_test.go +++ b/gno.land/cmd/gnoland/secrets_common_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,7 +26,7 @@ func TestCommon_SaveReadData(t *testing.T) { t.Run("invalid data read path", func(t *testing.T) { t.Parallel() - readData, err := readSecretData[p2p.NodeKey]("") + readData, err := readSecretData[types.NodeKey]("") assert.Nil(t, readData) assert.ErrorContains( @@ -44,7 +44,7 @@ func TestCommon_SaveReadData(t *testing.T) { require.NoError(t, saveSecretData("totally valid key", path)) - readData, err := readSecretData[p2p.NodeKey](path) + readData, err := readSecretData[types.NodeKey](path) require.Nil(t, readData) assert.ErrorContains(t, err, "unable to unmarshal data") @@ -59,7 +59,7 @@ func TestCommon_SaveReadData(t *testing.T) { require.NoError(t, saveSecretData(key, path)) - readKey, err := readSecretData[p2p.NodeKey](path) + readKey, err := readSecretData[types.NodeKey](path) require.NoError(t, err) assert.Equal(t, key, readKey) diff --git a/gno.land/cmd/gnoland/secrets_get.go b/gno.land/cmd/gnoland/secrets_get.go index 8d111516816..0a0a714f6ee 100644 --- a/gno.land/cmd/gnoland/secrets_get.go +++ b/gno.land/cmd/gnoland/secrets_get.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) var errInvalidSecretsGetArgs = errors.New("invalid number of secrets get arguments provided") @@ -169,7 +169,7 @@ func readValidatorState(path string) (*validatorStateInfo, error) { // readNodeID reads the node p2p info from the given path func readNodeID(path string) (*nodeIDInfo, error) { - nodeKey, err := readSecretData[p2p.NodeKey](path) + nodeKey, err := readSecretData[types.NodeKey](path) if err != nil { return nil, fmt.Errorf("unable to read node key, %w", err) } @@ -199,7 +199,7 @@ func readNodeID(path string) (*nodeIDInfo, error) { // constructP2PAddress constructs the P2P address other nodes can use // to connect directly -func constructP2PAddress(nodeID p2p.ID, listenAddress string) string { +func constructP2PAddress(nodeID types.ID, listenAddress string) string { var ( address string parts = strings.SplitN(listenAddress, "://", 2) diff --git a/gno.land/cmd/gnoland/secrets_get_test.go b/gno.land/cmd/gnoland/secrets_get_test.go index 66e6e3509fc..3dfe0c727dd 100644 --- a/gno.land/cmd/gnoland/secrets_get_test.go +++ b/gno.land/cmd/gnoland/secrets_get_test.go @@ -13,7 +13,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -66,7 +66,7 @@ func TestSecrets_Get_All(t *testing.T) { // Get the node key nodeKeyPath := filepath.Join(tempDir, defaultNodeKeyName) - nodeKey, err := readSecretData[p2p.NodeKey](nodeKeyPath) + nodeKey, err := readSecretData[types.NodeKey](nodeKeyPath) require.NoError(t, err) // Get the validator private key diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index 58dd0783f66..9a7ddd106c3 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) var errOverwriteNotEnabled = errors.New("overwrite not enabled") @@ -200,10 +200,6 @@ func generateLastSignValidatorState() *privval.FilePVLastSignState { } // generateNodeKey generates the p2p node key -func generateNodeKey() *p2p.NodeKey { - privKey := ed25519.GenPrivKey() - - return &p2p.NodeKey{ - PrivKey: privKey, - } +func generateNodeKey() *types.NodeKey { + return types.GenerateNodeKey() } diff --git a/gno.land/cmd/gnoland/secrets_init_test.go b/gno.land/cmd/gnoland/secrets_init_test.go index 20e061447f5..7be3650fb4b 100644 --- a/gno.land/cmd/gnoland/secrets_init_test.go +++ b/gno.land/cmd/gnoland/secrets_init_test.go @@ -7,7 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +37,7 @@ func verifyValidatorState(t *testing.T, path string) { func verifyNodeKey(t *testing.T, path string) { t.Helper() - nodeKey, err := readSecretData[p2p.NodeKey](path) + nodeKey, err := readSecretData[types.NodeKey](path) require.NoError(t, err) assert.NoError(t, validateNodeKey(nodeKey)) diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go index 32e563c1c6f..15fef6649ec 100644 --- a/gno.land/cmd/gnoland/secrets_verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) type secretsVerifyCfg struct { @@ -146,7 +146,7 @@ func readAndVerifyValidatorState(path string, io commands.IO) (*privval.FilePVLa // readAndVerifyNodeKey reads the node p2p key from the given path and verifies it func readAndVerifyNodeKey(path string, io commands.IO) error { - nodeKey, err := readSecretData[p2p.NodeKey](path) + nodeKey, err := readSecretData[types.NodeKey](path) if err != nil { return fmt.Errorf("unable to read node p2p key, %w", err) } diff --git a/gno.land/cmd/gnoland/secrets_verify_test.go b/gno.land/cmd/gnoland/secrets_verify_test.go index 513d7c8b503..67630aaaa4a 100644 --- a/gno.land/cmd/gnoland/secrets_verify_test.go +++ b/gno.land/cmd/gnoland/secrets_verify_test.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -347,7 +347,7 @@ func TestSecrets_Verify_Single(t *testing.T) { dirPath := t.TempDir() path := filepath.Join(dirPath, defaultNodeKeyName) - invalidNodeKey := &p2p.NodeKey{ + invalidNodeKey := &types.NodeKey{ PrivKey: nil, // invalid } diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 426a8c780c7..00099fbb1a5 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -16,7 +16,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/events" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) type InMemoryNodeConfig struct { @@ -131,7 +131,7 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, dbProvider := func(*node.DBContext) (db.DB, error) { return cfg.DB, nil } // Generate p2p node identity - nodekey := &p2p.NodeKey{PrivKey: ed25519.GenPrivKey()} + nodekey := &types.NodeKey{PrivKey: ed25519.GenPrivKey()} // Create and return the in-memory node instance return node.NewNode(cfg.TMConfig, diff --git a/go.mod b/go.mod index f73ba1926e6..5b469c415b6 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/rogpeppe/go-internal v1.12.0 github.com/rs/cors v1.11.1 github.com/rs/xid v1.6.0 + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 go.etcd.io/bbolt v1.3.11 diff --git a/go.sum b/go.sum index 78d60eeea90..53dbb1a5809 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/misc/autocounterd/go.mod b/misc/autocounterd/go.mod index 5de1d3c2974..13c461da65c 100644 --- a/misc/autocounterd/go.mod +++ b/misc/autocounterd/go.mod @@ -27,6 +27,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -44,6 +45,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index b34cbde0c00..284e58e3ae6 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -122,6 +122,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -178,6 +180,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/misc/loop/go.mod b/misc/loop/go.mod index f1c09cd9f82..51c5adb3eee 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -53,6 +53,7 @@ require ( github.com/prometheus/procfs v0.11.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect go.etcd.io/bbolt v1.3.11 // indirect @@ -69,6 +70,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 740cc629a21..af813be6b83 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -164,6 +164,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/tm2/pkg/bft/blockchain/pool.go b/tm2/pkg/bft/blockchain/pool.go index 5a82eb4d1d6..ded9282b7ec 100644 --- a/tm2/pkg/bft/blockchain/pool.go +++ b/tm2/pkg/bft/blockchain/pool.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/flow" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/service" ) @@ -69,7 +69,7 @@ type BlockPool struct { requesters map[int64]*bpRequester height int64 // the lowest key in requesters. // peers - peers map[p2p.ID]*bpPeer + peers map[p2pTypes.ID]*bpPeer maxPeerHeight int64 // the biggest reported height // atomic @@ -83,7 +83,7 @@ type BlockPool struct { // requests and errors will be sent to requestsCh and errorsCh accordingly. func NewBlockPool(start int64, requestsCh chan<- BlockRequest, errorsCh chan<- peerError) *BlockPool { bp := &BlockPool{ - peers: make(map[p2p.ID]*bpPeer), + peers: make(map[p2pTypes.ID]*bpPeer), requesters: make(map[int64]*bpRequester), height: start, @@ -226,13 +226,13 @@ func (pool *BlockPool) PopRequest() { // RedoRequest invalidates the block at pool.height, // Remove the peer and redo request from others. // Returns the ID of the removed peer. -func (pool *BlockPool) RedoRequest(height int64) p2p.ID { +func (pool *BlockPool) RedoRequest(height int64) p2pTypes.ID { pool.mtx.Lock() defer pool.mtx.Unlock() request := pool.requesters[height] peerID := request.getPeerID() - if peerID != p2p.ID("") { + if peerID != p2pTypes.ID("") { // RemovePeer will redo all requesters associated with this peer. pool.removePeer(peerID) } @@ -241,7 +241,7 @@ func (pool *BlockPool) RedoRequest(height int64) p2p.ID { // AddBlock validates that the block comes from the peer it was expected from and calls the requester to store it. // TODO: ensure that blocks come in order for each peer. -func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) { +func (pool *BlockPool) AddBlock(peerID p2pTypes.ID, block *types.Block, blockSize int) { pool.mtx.Lock() defer pool.mtx.Unlock() @@ -278,7 +278,7 @@ func (pool *BlockPool) MaxPeerHeight() int64 { } // SetPeerHeight sets the peer's alleged blockchain height. -func (pool *BlockPool) SetPeerHeight(peerID p2p.ID, height int64) { +func (pool *BlockPool) SetPeerHeight(peerID p2pTypes.ID, height int64) { pool.mtx.Lock() defer pool.mtx.Unlock() @@ -298,14 +298,14 @@ func (pool *BlockPool) SetPeerHeight(peerID p2p.ID, height int64) { // RemovePeer removes the peer with peerID from the pool. If there's no peer // with peerID, function is a no-op. -func (pool *BlockPool) RemovePeer(peerID p2p.ID) { +func (pool *BlockPool) RemovePeer(peerID p2pTypes.ID) { pool.mtx.Lock() defer pool.mtx.Unlock() pool.removePeer(peerID) } -func (pool *BlockPool) removePeer(peerID p2p.ID) { +func (pool *BlockPool) removePeer(peerID p2pTypes.ID) { for _, requester := range pool.requesters { if requester.getPeerID() == peerID { requester.redo(peerID) @@ -386,14 +386,14 @@ func (pool *BlockPool) requestersLen() int64 { return int64(len(pool.requesters)) } -func (pool *BlockPool) sendRequest(height int64, peerID p2p.ID) { +func (pool *BlockPool) sendRequest(height int64, peerID p2pTypes.ID) { if !pool.IsRunning() { return } pool.requestsCh <- BlockRequest{height, peerID} } -func (pool *BlockPool) sendError(err error, peerID p2p.ID) { +func (pool *BlockPool) sendError(err error, peerID p2pTypes.ID) { if !pool.IsRunning() { return } @@ -424,7 +424,7 @@ func (pool *BlockPool) debug() string { type bpPeer struct { pool *BlockPool - id p2p.ID + id p2pTypes.ID recvMonitor *flow.Monitor height int64 @@ -435,7 +435,7 @@ type bpPeer struct { logger *slog.Logger } -func newBPPeer(pool *BlockPool, peerID p2p.ID, height int64) *bpPeer { +func newBPPeer(pool *BlockPool, peerID p2pTypes.ID, height int64) *bpPeer { peer := &bpPeer{ pool: pool, id: peerID, @@ -499,10 +499,10 @@ type bpRequester struct { pool *BlockPool height int64 gotBlockCh chan struct{} - redoCh chan p2p.ID // redo may send multitime, add peerId to identify repeat + redoCh chan p2pTypes.ID // redo may send multitime, add peerId to identify repeat mtx sync.Mutex - peerID p2p.ID + peerID p2pTypes.ID block *types.Block } @@ -511,7 +511,7 @@ func newBPRequester(pool *BlockPool, height int64) *bpRequester { pool: pool, height: height, gotBlockCh: make(chan struct{}, 1), - redoCh: make(chan p2p.ID, 1), + redoCh: make(chan p2pTypes.ID, 1), peerID: "", block: nil, @@ -526,7 +526,7 @@ func (bpr *bpRequester) OnStart() error { } // Returns true if the peer matches and block doesn't already exist. -func (bpr *bpRequester) setBlock(block *types.Block, peerID p2p.ID) bool { +func (bpr *bpRequester) setBlock(block *types.Block, peerID p2pTypes.ID) bool { bpr.mtx.Lock() if bpr.block != nil || bpr.peerID != peerID { bpr.mtx.Unlock() @@ -548,7 +548,7 @@ func (bpr *bpRequester) getBlock() *types.Block { return bpr.block } -func (bpr *bpRequester) getPeerID() p2p.ID { +func (bpr *bpRequester) getPeerID() p2pTypes.ID { bpr.mtx.Lock() defer bpr.mtx.Unlock() return bpr.peerID @@ -570,7 +570,7 @@ func (bpr *bpRequester) reset() { // Tells bpRequester to pick another peer and try again. // NOTE: Nonblocking, and does nothing if another redo // was already requested. -func (bpr *bpRequester) redo(peerID p2p.ID) { +func (bpr *bpRequester) redo(peerID p2pTypes.ID) { select { case bpr.redoCh <- peerID: default: @@ -631,5 +631,5 @@ OUTER_LOOP: // delivering the block type BlockRequest struct { Height int64 - PeerID p2p.ID + PeerID p2pTypes.ID } diff --git a/tm2/pkg/bft/blockchain/pool_test.go b/tm2/pkg/bft/blockchain/pool_test.go index a4d5636d5e3..ee58d672e75 100644 --- a/tm2/pkg/bft/blockchain/pool_test.go +++ b/tm2/pkg/bft/blockchain/pool_test.go @@ -5,12 +5,12 @@ import ( "testing" "time" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/gnolang/gno/tm2/pkg/random" ) @@ -19,7 +19,7 @@ func init() { } type testPeer struct { - id p2p.ID + id p2pTypes.ID height int64 inputChan chan inputData // make sure each peer's data is sequential } @@ -47,7 +47,7 @@ func (p testPeer) simulateInput(input inputData) { // input.t.Logf("Added block from peer %v (height: %v)", input.request.PeerID, input.request.Height) } -type testPeers map[p2p.ID]testPeer +type testPeers map[p2pTypes.ID]testPeer func (ps testPeers) start() { for _, v := range ps { @@ -64,7 +64,7 @@ func (ps testPeers) stop() { func makePeers(numPeers int, minHeight, maxHeight int64) testPeers { peers := make(testPeers, numPeers) for i := 0; i < numPeers; i++ { - peerID := p2p.ID(random.RandStr(12)) + peerID := p2pTypes.ID(random.RandStr(12)) height := minHeight + random.RandInt63n(maxHeight-minHeight) peers[peerID] = testPeer{peerID, height, make(chan inputData, 10)} } @@ -172,7 +172,7 @@ func TestBlockPoolTimeout(t *testing.T) { // Pull from channels counter := 0 - timedOut := map[p2p.ID]struct{}{} + timedOut := map[p2pTypes.ID]struct{}{} for { select { case err := <-errorsCh: @@ -195,7 +195,7 @@ func TestBlockPoolRemovePeer(t *testing.T) { peers := make(testPeers, 10) for i := 0; i < 10; i++ { - peerID := p2p.ID(fmt.Sprintf("%d", i+1)) + peerID := p2pTypes.ID(fmt.Sprintf("%d", i+1)) height := int64(i + 1) peers[peerID] = testPeer{peerID, height, make(chan inputData)} } @@ -215,10 +215,10 @@ func TestBlockPoolRemovePeer(t *testing.T) { assert.EqualValues(t, 10, pool.MaxPeerHeight()) // remove not-existing peer - assert.NotPanics(t, func() { pool.RemovePeer(p2p.ID("Superman")) }) + assert.NotPanics(t, func() { pool.RemovePeer(p2pTypes.ID("Superman")) }) // remove peer with biggest height - pool.RemovePeer(p2p.ID("10")) + pool.RemovePeer(p2pTypes.ID("10")) assert.EqualValues(t, 9, pool.MaxPeerHeight()) // remove all peers diff --git a/tm2/pkg/bft/blockchain/reactor.go b/tm2/pkg/bft/blockchain/reactor.go index 09e1225b717..bac27f3959e 100644 --- a/tm2/pkg/bft/blockchain/reactor.go +++ b/tm2/pkg/bft/blockchain/reactor.go @@ -12,6 +12,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/store" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" ) const ( @@ -37,15 +38,11 @@ const ( bcBlockResponseMessageFieldKeySize ) -type consensusReactor interface { - // for when we switch from blockchain reactor and fast sync to - // the consensus machine - SwitchToConsensus(sm.State, int) -} +type SwitchToConsensusFn func(sm.State, int) type peerError struct { err error - peerID p2p.ID + peerID p2pTypes.ID } func (e peerError) Error() string { @@ -66,11 +63,17 @@ type BlockchainReactor struct { requestsCh <-chan BlockRequest errorsCh <-chan peerError + + switchToConsensusFn SwitchToConsensusFn } // NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, +func NewBlockchainReactor( + state sm.State, + blockExec *sm.BlockExecutor, + store *store.BlockStore, fastSync bool, + switchToConsensusFn SwitchToConsensusFn, ) *BlockchainReactor { if state.LastBlockHeight != store.Height() { panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, @@ -89,13 +92,14 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *st ) bcR := &BlockchainReactor{ - initialState: state, - blockExec: blockExec, - store: store, - pool: pool, - fastSync: fastSync, - requestsCh: requestsCh, - errorsCh: errorsCh, + initialState: state, + blockExec: blockExec, + store: store, + pool: pool, + fastSync: fastSync, + requestsCh: requestsCh, + errorsCh: errorsCh, + switchToConsensusFn: switchToConsensusFn, } bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) return bcR @@ -257,16 +261,13 @@ FOR_LOOP: select { case <-switchToConsensusTicker.C: height, numPending, lenRequesters := bcR.pool.GetStatus() - outbound, inbound, _ := bcR.Switch.NumPeers() - bcR.Logger.Debug("Consensus ticker", "numPending", numPending, "total", lenRequesters, - "outbound", outbound, "inbound", inbound) + + bcR.Logger.Debug("Consensus ticker", "numPending", numPending, "total", lenRequesters) if bcR.pool.IsCaughtUp() { bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) bcR.pool.Stop() - conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) - if ok { - conR.SwitchToConsensus(state, blocksSynced) - } + + bcR.switchToConsensusFn(state, blocksSynced) // else { // should only happen during testing // } diff --git a/tm2/pkg/bft/blockchain/reactor_test.go b/tm2/pkg/bft/blockchain/reactor_test.go index a40dbc6376b..1bc2df59055 100644 --- a/tm2/pkg/bft/blockchain/reactor_test.go +++ b/tm2/pkg/bft/blockchain/reactor_test.go @@ -1,14 +1,13 @@ package blockchain import ( + "context" "log/slog" "os" "sort" "testing" "time" - "github.com/stretchr/testify/assert" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/appconn" cfg "github.com/gnolang/gno/tm2/pkg/bft/config" @@ -20,9 +19,12 @@ import ( tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/errors" + p2pTesting "github.com/gnolang/gno/tm2/pkg/internal/p2p" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" ) var config *cfg.Config @@ -110,7 +112,7 @@ func newBlockchainReactor(logger *slog.Logger, genDoc *types.GenesisDoc, privVal blockStore.SaveBlock(thisBlock, thisParts, lastCommit) } - bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync, nil) bcReactor.SetLogger(logger.With("module", "blockchain")) return BlockchainReactorPair{bcReactor, proxyApp} @@ -125,15 +127,35 @@ func TestNoBlockResponse(t *testing.T) { maxBlockHeight := int64(65) - reactorPairs := make([]BlockchainReactorPair, 2) + var ( + reactorPairs = make([]BlockchainReactorPair, 2) + options = make(map[int][]p2p.SwitchOption) + ) - reactorPairs[0] = newBlockchainReactor(log.NewTestingLogger(t), genDoc, privVals, maxBlockHeight) - reactorPairs[1] = newBlockchainReactor(log.NewTestingLogger(t), genDoc, privVals, 0) + for i := range reactorPairs { + height := int64(0) + if i == 0 { + height = maxBlockHeight + } - p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("BLOCKCHAIN", reactorPairs[i].reactor) - return s - }, p2p.Connect2Switches) + reactorPairs[i] = newBlockchainReactor(log.NewTestingLogger(t), genDoc, privVals, height) + + options[i] = []p2p.SwitchOption{ + p2p.WithReactor("BLOCKCHAIN", reactorPairs[i].reactor), + } + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + testingCfg := p2pTesting.TestingConfig{ + Count: 2, + P2PCfg: config.P2P, + SwitchOptions: options, + Channels: []byte{BlockchainChannel}, + } + + p2pTesting.MakeConnectedPeers(t, ctx, testingCfg) defer func() { for _, r := range reactorPairs { @@ -194,17 +216,35 @@ func TestFlappyBadBlockStopsPeer(t *testing.T) { otherChain.app.Stop() }() - reactorPairs := make([]BlockchainReactorPair, 4) + var ( + reactorPairs = make([]BlockchainReactorPair, 4) + options = make(map[int][]p2p.SwitchOption) + ) + + for i := range reactorPairs { + height := int64(0) + if i == 0 { + height = maxBlockHeight + } + + reactorPairs[i] = newBlockchainReactor(log.NewNoopLogger(), genDoc, privVals, height) - reactorPairs[0] = newBlockchainReactor(log.NewNoopLogger(), genDoc, privVals, maxBlockHeight) - reactorPairs[1] = newBlockchainReactor(log.NewNoopLogger(), genDoc, privVals, 0) - reactorPairs[2] = newBlockchainReactor(log.NewNoopLogger(), genDoc, privVals, 0) - reactorPairs[3] = newBlockchainReactor(log.NewNoopLogger(), genDoc, privVals, 0) + options[i] = []p2p.SwitchOption{ + p2p.WithReactor("BLOCKCHAIN", reactorPairs[i].reactor), + } + } - switches := p2p.MakeConnectedSwitches(config.P2P, 4, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("BLOCKCHAIN", reactorPairs[i].reactor) - return s - }, p2p.Connect2Switches) + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + testingCfg := p2pTesting.TestingConfig{ + Count: 4, + P2PCfg: config.P2P, + SwitchOptions: options, + Channels: []byte{BlockchainChannel}, + } + + switches, transports := p2pTesting.MakeConnectedPeers(t, ctx, testingCfg) defer func() { for _, r := range reactorPairs { @@ -222,7 +262,7 @@ func TestFlappyBadBlockStopsPeer(t *testing.T) { } // at this time, reactors[0-3] is the newest - assert.Equal(t, 3, reactorPairs[1].reactor.Switch.Peers().Size()) + assert.Equal(t, 3, len(reactorPairs[1].reactor.Switch.Peers().List())) // mark reactorPairs[3] is an invalid peer reactorPairs[3].reactor.store = otherChain.reactor.store @@ -230,24 +270,41 @@ func TestFlappyBadBlockStopsPeer(t *testing.T) { lastReactorPair := newBlockchainReactor(log.NewNoopLogger(), genDoc, privVals, 0) reactorPairs = append(reactorPairs, lastReactorPair) - switches = append(switches, p2p.MakeConnectedSwitches(config.P2P, 1, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("BLOCKCHAIN", reactorPairs[len(reactorPairs)-1].reactor) - return s - }, p2p.Connect2Switches)...) + persistentPeers := make([]*p2pTypes.NetAddress, 0, len(transports)) - for i := 0; i < len(reactorPairs)-1; i++ { - p2p.Connect2Switches(switches, i, len(reactorPairs)-1) + for _, tr := range transports { + addr := tr.NetAddress() + persistentPeers = append(persistentPeers, &addr) } + for i, opt := range options { + opt = append(opt, p2p.WithPersistentPeers(persistentPeers)) + + options[i] = opt + } + + ctx, cancelFn = context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + testingCfg = p2pTesting.TestingConfig{ + Count: 1, + P2PCfg: config.P2P, + SwitchOptions: options, + Channels: []byte{BlockchainChannel}, + } + + sw, _ := p2pTesting.MakeConnectedPeers(t, ctx, testingCfg) + switches = append(switches, sw...) + for { - if lastReactorPair.reactor.pool.IsCaughtUp() || lastReactorPair.reactor.Switch.Peers().Size() == 0 { + if lastReactorPair.reactor.pool.IsCaughtUp() || len(lastReactorPair.reactor.Switch.Peers().List()) == 0 { break } time.Sleep(1 * time.Second) } - assert.True(t, lastReactorPair.reactor.Switch.Peers().Size() < len(reactorPairs)-1) + assert.True(t, len(lastReactorPair.reactor.Switch.Peers().List()) < len(reactorPairs)-1) } func TestBcBlockRequestMessageValidateBasic(t *testing.T) { diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index f9e9a0cd899..b4f2948b12a 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -6,6 +6,7 @@ import ( "path/filepath" "regexp" "slices" + "time" "dario.cat/mergo" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -163,12 +164,21 @@ func LoadOrMakeConfigWithOptions(root string, opts ...Option) (*Config, error) { return cfg, nil } +// testP2PConfig returns a configuration for testing the peer-to-peer layer +func testP2PConfig() *p2p.P2PConfig { + cfg := p2p.DefaultP2PConfig() + cfg.ListenAddress = "tcp://0.0.0.0:26656" + cfg.FlushThrottleTimeout = 10 * time.Millisecond + + return cfg +} + // TestConfig returns a configuration that can be used for testing func TestConfig() *Config { return &Config{ BaseConfig: testBaseConfig(), RPC: rpc.TestRPCConfig(), - P2P: p2p.TestP2PConfig(), + P2P: testP2PConfig(), Mempool: mem.TestMempoolConfig(), Consensus: cns.TestConsensusConfig(), TxEventStore: eventstore.DefaultEventStoreConfig(), @@ -318,10 +328,6 @@ type BaseConfig struct { // TCP or UNIX socket address for the profiling server to listen on ProfListenAddress string `toml:"prof_laddr" comment:"TCP or UNIX socket address for the profiling server to listen on"` - - // If true, query the ABCI app on connecting to a new peer - // so the app can decide if we should keep the connection or not - FilterPeers bool `toml:"filter_peers" comment:"If true, query the ABCI app on connecting to a new peer\n so the app can decide if we should keep the connection or not"` // false } // DefaultBaseConfig returns a default base configuration for a Tendermint node @@ -335,7 +341,6 @@ func DefaultBaseConfig() BaseConfig { ABCI: SocketABCI, ProfListenAddress: "", FastSyncMode: true, - FilterPeers: false, DBBackend: db.GoLevelDBBackend.String(), DBPath: DefaultDBDir, } diff --git a/tm2/pkg/bft/consensus/reactor.go b/tm2/pkg/bft/consensus/reactor.go index aee695114f8..8deca2b1e7a 100644 --- a/tm2/pkg/bft/consensus/reactor.go +++ b/tm2/pkg/bft/consensus/reactor.go @@ -35,7 +35,7 @@ const ( // ConsensusReactor defines a reactor for the consensus service. type ConsensusReactor struct { - p2p.BaseReactor // BaseService + p2p.Switch + p2p.BaseReactor // BaseService + p2p.MultiplexSwitch conS *ConsensusState @@ -417,7 +417,7 @@ func (conR *ConsensusReactor) broadcastHasVoteMessage(vote *types.Vote) { conR.Switch.Broadcast(StateChannel, amino.MustMarshalAny(msg)) /* // TODO: Make this broadcast more selective. - for _, peer := range conR.Switch.Peers().List() { + for _, peer := range conR.MultiplexSwitch.Peers().List() { ps, ok := peer.Get(PeerStateKey).(*PeerState) if !ok { panic(fmt.Sprintf("Peer %v has no state", peer)) @@ -826,12 +826,12 @@ func (conR *ConsensusReactor) peerStatsRoutine() { case *VoteMessage: if numVotes := ps.RecordVote(); numVotes%votesToContributeToBecomeGoodPeer == 0 { // TODO: peer metrics. - // conR.Switch.MarkPeerAsGood(peer) + // conR.MultiplexSwitch.MarkPeerAsGood(peer) } case *BlockPartMessage: if numParts := ps.RecordBlockPart(); numParts%blocksToContributeToBecomeGoodPeer == 0 { // TODO: peer metrics. - // conR.Switch.MarkPeerAsGood(peer) + // conR.MultiplexSwitch.MarkPeerAsGood(peer) } } case <-conR.conS.Quit(): diff --git a/tm2/pkg/bft/consensus/reactor_test.go b/tm2/pkg/bft/consensus/reactor_test.go index 42f944b7481..0e1d6249783 100644 --- a/tm2/pkg/bft/consensus/reactor_test.go +++ b/tm2/pkg/bft/consensus/reactor_test.go @@ -1,14 +1,13 @@ package consensus import ( + "context" "fmt" "log/slog" "sync" "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/abci/example/kvstore" cfg "github.com/gnolang/gno/tm2/pkg/bft/config" @@ -18,27 +17,39 @@ import ( "github.com/gnolang/gno/tm2/pkg/bitarray" "github.com/gnolang/gno/tm2/pkg/crypto/tmhash" "github.com/gnolang/gno/tm2/pkg/events" + p2pTesting "github.com/gnolang/gno/tm2/pkg/internal/p2p" "github.com/gnolang/gno/tm2/pkg/log" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/p2p" - "github.com/gnolang/gno/tm2/pkg/p2p/mock" "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" ) // ---------------------------------------------- // in-process testnets -func startConsensusNet(css []*ConsensusState, n int) ([]*ConsensusReactor, []<-chan events.Event, []events.EventSwitch, []*p2p.Switch) { +func startConsensusNet( + t *testing.T, + css []*ConsensusState, + n int, +) ([]*ConsensusReactor, []<-chan events.Event, []events.EventSwitch, []*p2p.MultiplexSwitch) { + t.Helper() + reactors := make([]*ConsensusReactor, n) blocksSubs := make([]<-chan events.Event, 0) eventSwitches := make([]events.EventSwitch, n) - p2pSwitches := ([]*p2p.Switch)(nil) + p2pSwitches := ([]*p2p.MultiplexSwitch)(nil) + options := make(map[int][]p2p.SwitchOption) for i := 0; i < n; i++ { /*logger, err := tmflags.ParseLogLevel("consensus:info,*:error", logger, "info") if err != nil { t.Fatal(err)}*/ reactors[i] = NewConsensusReactor(css[i], true) // so we dont start the consensus states reactors[i].SetLogger(css[i].Logger) + options[i] = []p2p.SwitchOption{ + p2p.WithReactor("CONSENSUS", reactors[i]), + } + // evsw is already started with the cs eventSwitches[i] = css[i].evsw reactors[i].SetEventSwitch(eventSwitches[i]) @@ -51,11 +62,22 @@ func startConsensusNet(css []*ConsensusState, n int) ([]*ConsensusReactor, []<-c } } // make connected switches and start all reactors - p2pSwitches = p2p.MakeConnectedSwitches(config.P2P, n, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("CONSENSUS", reactors[i]) - s.SetLogger(reactors[i].conS.Logger.With("module", "p2p")) - return s - }, p2p.Connect2Switches) + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + testingCfg := p2pTesting.TestingConfig{ + P2PCfg: config.P2P, + Count: n, + SwitchOptions: options, + Channels: []byte{ + StateChannel, + DataChannel, + VoteChannel, + VoteSetBitsChannel, + }, + } + + p2pSwitches, _ = p2pTesting.MakeConnectedPeers(t, ctx, testingCfg) // now that everyone is connected, start the state machines // If we started the state machines before everyone was connected, @@ -68,11 +90,15 @@ func startConsensusNet(css []*ConsensusState, n int) ([]*ConsensusReactor, []<-c return reactors, blocksSubs, eventSwitches, p2pSwitches } -func stopConsensusNet(logger *slog.Logger, reactors []*ConsensusReactor, eventSwitches []events.EventSwitch, p2pSwitches []*p2p.Switch) { +func stopConsensusNet( + logger *slog.Logger, + reactors []*ConsensusReactor, + eventSwitches []events.EventSwitch, + p2pSwitches []*p2p.MultiplexSwitch, +) { logger.Info("stopConsensusNet", "n", len(reactors)) - for i, r := range reactors { + for i := range reactors { logger.Info("stopConsensusNet: Stopping ConsensusReactor", "i", i) - r.Switch.Stop() } for i, b := range eventSwitches { logger.Info("stopConsensusNet: Stopping evsw", "i", i) @@ -92,7 +118,7 @@ func TestReactorBasic(t *testing.T) { N := 4 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() - reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(css, N) + reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(t, css, N) defer stopConsensusNet(log.NewTestingLogger(t), reactors, eventSwitches, p2pSwitches) // wait till everyone makes the first new block timeoutWaitGroup(t, N, func(j int) { @@ -112,7 +138,7 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { c.Consensus.CreateEmptyBlocks = false }) defer cleanup() - reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(css, N) + reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(t, css, N) defer stopConsensusNet(log.NewTestingLogger(t), reactors, eventSwitches, p2pSwitches) // send a tx @@ -132,12 +158,12 @@ func TestReactorReceiveDoesNotPanicIfAddPeerHasntBeenCalledYet(t *testing.T) { N := 1 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() - reactors, _, eventSwitches, p2pSwitches := startConsensusNet(css, N) + reactors, _, eventSwitches, p2pSwitches := startConsensusNet(t, css, N) defer stopConsensusNet(log.NewTestingLogger(t), reactors, eventSwitches, p2pSwitches) var ( reactor = reactors[0] - peer = mock.NewPeer(nil) + peer = p2pTesting.NewPeer(t) msg = amino.MustMarshalAny(&HasVoteMessage{Height: 1, Round: 1, Index: 1, Type: types.PrevoteType}) ) @@ -156,12 +182,12 @@ func TestReactorReceivePanicsIfInitPeerHasntBeenCalledYet(t *testing.T) { N := 1 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() - reactors, _, eventSwitches, p2pSwitches := startConsensusNet(css, N) + reactors, _, eventSwitches, p2pSwitches := startConsensusNet(t, css, N) defer stopConsensusNet(log.NewTestingLogger(t), reactors, eventSwitches, p2pSwitches) var ( reactor = reactors[0] - peer = mock.NewPeer(nil) + peer = p2pTesting.NewPeer(t) msg = amino.MustMarshalAny(&HasVoteMessage{Height: 1, Round: 1, Index: 1, Type: types.PrevoteType}) ) @@ -182,7 +208,7 @@ func TestFlappyReactorRecordsVotesAndBlockParts(t *testing.T) { N := 4 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() - reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(css, N) + reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(t, css, N) defer stopConsensusNet(log.NewTestingLogger(t), reactors, eventSwitches, p2pSwitches) // wait till everyone makes the first new block @@ -210,7 +236,7 @@ func TestReactorVotingPowerChange(t *testing.T) { css, cleanup := randConsensusNet(nVals, "consensus_voting_power_changes_test", newMockTickerFunc(true), newPersistentKVStore) defer cleanup() - reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(css, nVals) + reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(t, css, nVals) defer stopConsensusNet(logger, reactors, eventSwitches, p2pSwitches) // map of active validators @@ -276,7 +302,7 @@ func TestReactorValidatorSetChanges(t *testing.T) { logger := log.NewTestingLogger(t) - reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(css, nPeers) + reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(t, css, nPeers) defer stopConsensusNet(logger, reactors, eventSwitches, p2pSwitches) // map of active validators @@ -375,7 +401,7 @@ func TestReactorWithTimeoutCommit(t *testing.T) { css[i].config.SkipTimeoutCommit = false } - reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(css, N-1) + reactors, blocksSubs, eventSwitches, p2pSwitches := startConsensusNet(t, css, N-1) defer stopConsensusNet(log.NewTestingLogger(t), reactors, eventSwitches, p2pSwitches) // wait till everyone makes the first new block diff --git a/tm2/pkg/bft/consensus/state.go b/tm2/pkg/bft/consensus/state.go index 8b2653813e3..d9c78ec1bdf 100644 --- a/tm2/pkg/bft/consensus/state.go +++ b/tm2/pkg/bft/consensus/state.go @@ -23,7 +23,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/events" osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/service" "github.com/gnolang/gno/tm2/pkg/telemetry" "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" @@ -53,7 +53,7 @@ type newRoundStepInfo struct { // msgs from the reactor which may update the state type msgInfo struct { Msg ConsensusMessage `json:"msg"` - PeerID p2p.ID `json:"peer_key"` + PeerID p2pTypes.ID `json:"peer_key"` } // WAL message. @@ -399,7 +399,7 @@ func (cs *ConsensusState) OpenWAL(walFile string) (walm.WAL, error) { // TODO: should these return anything or let callers just use events? // AddVote inputs a vote. -func (cs *ConsensusState) AddVote(vote *types.Vote, peerID p2p.ID) (added bool, err error) { +func (cs *ConsensusState) AddVote(vote *types.Vote, peerID p2pTypes.ID) (added bool, err error) { if peerID == "" { cs.internalMsgQueue <- msgInfo{&VoteMessage{vote}, ""} } else { @@ -411,7 +411,7 @@ func (cs *ConsensusState) AddVote(vote *types.Vote, peerID p2p.ID) (added bool, } // SetProposal inputs a proposal. -func (cs *ConsensusState) SetProposal(proposal *types.Proposal, peerID p2p.ID) error { +func (cs *ConsensusState) SetProposal(proposal *types.Proposal, peerID p2pTypes.ID) error { if peerID == "" { cs.internalMsgQueue <- msgInfo{&ProposalMessage{proposal}, ""} } else { @@ -423,7 +423,7 @@ func (cs *ConsensusState) SetProposal(proposal *types.Proposal, peerID p2p.ID) e } // AddProposalBlockPart inputs a part of the proposal block. -func (cs *ConsensusState) AddProposalBlockPart(height int64, round int, part *types.Part, peerID p2p.ID) error { +func (cs *ConsensusState) AddProposalBlockPart(height int64, round int, part *types.Part, peerID p2pTypes.ID) error { if peerID == "" { cs.internalMsgQueue <- msgInfo{&BlockPartMessage{height, round, part}, ""} } else { @@ -435,7 +435,7 @@ func (cs *ConsensusState) AddProposalBlockPart(height int64, round int, part *ty } // SetProposalAndBlock inputs the proposal and all block parts. -func (cs *ConsensusState) SetProposalAndBlock(proposal *types.Proposal, block *types.Block, parts *types.PartSet, peerID p2p.ID) error { +func (cs *ConsensusState) SetProposalAndBlock(proposal *types.Proposal, block *types.Block, parts *types.PartSet, peerID p2pTypes.ID) error { if err := cs.SetProposal(proposal, peerID); err != nil { return err } @@ -1444,7 +1444,7 @@ func (cs *ConsensusState) defaultSetProposal(proposal *types.Proposal) error { // NOTE: block is not necessarily valid. // Asynchronously triggers either enterPrevote (before we timeout of propose) or tryFinalizeCommit, once we have the full block. -func (cs *ConsensusState) addProposalBlockPart(msg *BlockPartMessage, peerID p2p.ID) (added bool, err error) { +func (cs *ConsensusState) addProposalBlockPart(msg *BlockPartMessage, peerID p2pTypes.ID) (added bool, err error) { height, round, part := msg.Height, msg.Round, msg.Part // Blocks might be reused, so round mismatch is OK @@ -1514,7 +1514,7 @@ func (cs *ConsensusState) addProposalBlockPart(msg *BlockPartMessage, peerID p2p } // Attempt to add the vote. if its a duplicate signature, dupeout the validator -func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, error) { +func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2pTypes.ID) (bool, error) { added, err := cs.addVote(vote, peerID) if err != nil { // If the vote height is off, we'll just ignore it, @@ -1547,7 +1547,7 @@ func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, err // ----------------------------------------------------------------------------- -func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, err error) { +func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2pTypes.ID) (added bool, err error) { cs.Logger.Debug("addVote", "voteHeight", vote.Height, "voteType", vote.Type, "valIndex", vote.ValidatorIndex, "csHeight", cs.Height) // A precommit for the previous height? diff --git a/tm2/pkg/bft/consensus/state_test.go b/tm2/pkg/bft/consensus/state_test.go index 201cf8906b3..8c340d12eae 100644 --- a/tm2/pkg/bft/consensus/state_test.go +++ b/tm2/pkg/bft/consensus/state_test.go @@ -1733,7 +1733,7 @@ func TestStateOutputsBlockPartsStats(t *testing.T) { // create dummy peer cs, _ := randConsensusState(1) - peer := p2pmock.NewPeer(nil) + peer := p2pmock.Peer{} // 1) new block part parts := types.NewPartSetFromData(random.RandBytes(100), 10) @@ -1777,7 +1777,7 @@ func TestStateOutputVoteStats(t *testing.T) { cs, vss := randConsensusState(2) // create dummy peer - peer := p2pmock.NewPeer(nil) + peer := &p2pmock.Peer{} vote := signVote(vss[1], types.PrecommitType, []byte("test"), types.PartSetHeader{}) diff --git a/tm2/pkg/bft/consensus/types/height_vote_set.go b/tm2/pkg/bft/consensus/types/height_vote_set.go index b81937ebd1e..7f3d52022ad 100644 --- a/tm2/pkg/bft/consensus/types/height_vote_set.go +++ b/tm2/pkg/bft/consensus/types/height_vote_set.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" ) type RoundVoteSet struct { @@ -39,9 +39,9 @@ type HeightVoteSet struct { valSet *types.ValidatorSet mtx sync.Mutex - round int // max tracked round - roundVoteSets map[int]RoundVoteSet // keys: [0...round] - peerCatchupRounds map[p2p.ID][]int // keys: peer.ID; values: at most 2 rounds + round int // max tracked round + roundVoteSets map[int]RoundVoteSet // keys: [0...round] + peerCatchupRounds map[p2pTypes.ID][]int // keys: peer.ID; values: at most 2 rounds } func NewHeightVoteSet(chainID string, height int64, valSet *types.ValidatorSet) *HeightVoteSet { @@ -59,7 +59,7 @@ func (hvs *HeightVoteSet) Reset(height int64, valSet *types.ValidatorSet) { hvs.height = height hvs.valSet = valSet hvs.roundVoteSets = make(map[int]RoundVoteSet) - hvs.peerCatchupRounds = make(map[p2p.ID][]int) + hvs.peerCatchupRounds = make(map[p2pTypes.ID][]int) hvs.addRound(0) hvs.round = 0 @@ -108,7 +108,7 @@ func (hvs *HeightVoteSet) addRound(round int) { // Duplicate votes return added=false, err=nil. // By convention, peerID is "" if origin is self. -func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerID p2p.ID) (added bool, err error) { +func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerID p2pTypes.ID) (added bool, err error) { hvs.mtx.Lock() defer hvs.mtx.Unlock() if !types.IsVoteTypeValid(vote.Type) { @@ -176,7 +176,7 @@ func (hvs *HeightVoteSet) getVoteSet(round int, type_ types.SignedMsgType) *type // NOTE: if there are too many peers, or too much peer churn, // this can cause memory issues. // TODO: implement ability to remove peers too -func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ types.SignedMsgType, peerID p2p.ID, blockID types.BlockID) error { +func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ types.SignedMsgType, peerID p2pTypes.ID, blockID types.BlockID) error { hvs.mtx.Lock() defer hvs.mtx.Unlock() if !types.IsVoteTypeValid(type_) { diff --git a/tm2/pkg/bft/mempool/reactor.go b/tm2/pkg/bft/mempool/reactor.go index 3ef85b80a21..acb4e351f3f 100644 --- a/tm2/pkg/bft/mempool/reactor.go +++ b/tm2/pkg/bft/mempool/reactor.go @@ -13,6 +13,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/clist" "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" ) const ( @@ -39,19 +40,19 @@ type Reactor struct { type mempoolIDs struct { mtx sync.RWMutex - peerMap map[p2p.ID]uint16 + peerMap map[p2pTypes.ID]uint16 nextID uint16 // assumes that a node will never have over 65536 active peers activeIDs map[uint16]struct{} // used to check if a given peerID key is used, the value doesn't matter } // Reserve searches for the next unused ID and assigns it to the // peer. -func (ids *mempoolIDs) ReserveForPeer(peer p2p.Peer) { +func (ids *mempoolIDs) ReserveForPeer(id p2pTypes.ID) { ids.mtx.Lock() defer ids.mtx.Unlock() curID := ids.nextPeerID() - ids.peerMap[peer.ID()] = curID + ids.peerMap[id] = curID ids.activeIDs[curID] = struct{}{} } @@ -73,28 +74,28 @@ func (ids *mempoolIDs) nextPeerID() uint16 { } // Reclaim returns the ID reserved for the peer back to unused pool. -func (ids *mempoolIDs) Reclaim(peer p2p.Peer) { +func (ids *mempoolIDs) Reclaim(id p2pTypes.ID) { ids.mtx.Lock() defer ids.mtx.Unlock() - removedID, ok := ids.peerMap[peer.ID()] + removedID, ok := ids.peerMap[id] if ok { delete(ids.activeIDs, removedID) - delete(ids.peerMap, peer.ID()) + delete(ids.peerMap, id) } } // GetForPeer returns an ID reserved for the peer. -func (ids *mempoolIDs) GetForPeer(peer p2p.Peer) uint16 { +func (ids *mempoolIDs) GetForPeer(id p2pTypes.ID) uint16 { ids.mtx.RLock() defer ids.mtx.RUnlock() - return ids.peerMap[peer.ID()] + return ids.peerMap[id] } func newMempoolIDs() *mempoolIDs { return &mempoolIDs{ - peerMap: make(map[p2p.ID]uint16), + peerMap: make(map[p2pTypes.ID]uint16), activeIDs: map[uint16]struct{}{0: {}}, nextID: 1, // reserve unknownPeerID(0) for mempoolReactor.BroadcastTx } @@ -139,13 +140,13 @@ func (memR *Reactor) GetChannels() []*p2p.ChannelDescriptor { // AddPeer implements Reactor. // It starts a broadcast routine ensuring all txs are forwarded to the given peer. func (memR *Reactor) AddPeer(peer p2p.Peer) { - memR.ids.ReserveForPeer(peer) + memR.ids.ReserveForPeer(peer.ID()) go memR.broadcastTxRoutine(peer) } // RemovePeer implements Reactor. func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { - memR.ids.Reclaim(peer) + memR.ids.Reclaim(peer.ID()) // broadcast routine checks if peer is gone and returns } @@ -162,7 +163,7 @@ func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { switch msg := msg.(type) { case *TxMessage: - peerID := memR.ids.GetForPeer(src) + peerID := memR.ids.GetForPeer(src.ID()) err := memR.mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{SenderID: peerID}) if err != nil { memR.Logger.Info("Could not check tx", "tx", txID(msg.Tx), "err", err) @@ -184,7 +185,7 @@ func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) { return } - peerID := memR.ids.GetForPeer(peer) + peerID := memR.ids.GetForPeer(peer.ID()) var next *clist.CElement for { // In case of both next.NextWaitChan() and peer.Quit() are variable at the same time @@ -213,7 +214,7 @@ func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) { peerState, ok := peer.Get(types.PeerStateKey).(PeerState) if !ok { // Peer does not have a state yet. We set it in the consensus reactor, but - // when we add peer in Switch, the order we call reactors#AddPeer is + // when we add peer in MultiplexSwitch, the order we call reactors#AddPeer is // different every time due to us using a map. Sometimes other reactors // will be initialized before the consensus reactor. We should wait a few // milliseconds and retry. diff --git a/tm2/pkg/bft/mempool/reactor_test.go b/tm2/pkg/bft/mempool/reactor_test.go index e7a3c43a6b9..2d20fb252e2 100644 --- a/tm2/pkg/bft/mempool/reactor_test.go +++ b/tm2/pkg/bft/mempool/reactor_test.go @@ -1,26 +1,36 @@ package mempool import ( - "net" + "context" + "fmt" "sync" "testing" "time" "github.com/fortytw2/leaktest" - "github.com/stretchr/testify/assert" - "github.com/gnolang/gno/tm2/pkg/bft/abci/example/kvstore" memcfg "github.com/gnolang/gno/tm2/pkg/bft/mempool/config" "github.com/gnolang/gno/tm2/pkg/bft/proxy" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/errors" + p2pTesting "github.com/gnolang/gno/tm2/pkg/internal/p2p" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/p2p" p2pcfg "github.com/gnolang/gno/tm2/pkg/p2p/config" - "github.com/gnolang/gno/tm2/pkg/p2p/mock" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" ) +// testP2PConfig returns a configuration for testing the peer-to-peer layer +func testP2PConfig() *p2pcfg.P2PConfig { + cfg := p2pcfg.DefaultP2PConfig() + cfg.ListenAddress = "tcp://0.0.0.0:26656" + cfg.FlushThrottleTimeout = 10 * time.Millisecond + + return cfg +} + type peerState struct { height int64 } @@ -30,65 +40,108 @@ func (ps peerState) GetHeight() int64 { } // connect N mempool reactors through N switches -func makeAndConnectReactors(mconfig *memcfg.MempoolConfig, pconfig *p2pcfg.P2PConfig, n int) []*Reactor { - reactors := make([]*Reactor, n) - logger := log.NewNoopLogger() +func makeAndConnectReactors(t *testing.T, mconfig *memcfg.MempoolConfig, pconfig *p2pcfg.P2PConfig, n int) []*Reactor { + t.Helper() + + var ( + reactors = make([]*Reactor, n) + logger = log.NewNoopLogger() + options = make(map[int][]p2p.SwitchOption) + ) + for i := 0; i < n; i++ { app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool, cleanup := newMempoolWithApp(cc) defer cleanup() - reactors[i] = NewReactor(mconfig, mempool) // so we dont start the consensus states - reactors[i].SetLogger(logger.With("validator", i)) + reactor := NewReactor(mconfig, mempool) // so we dont start the consensus states + reactor.SetLogger(logger.With("validator", i)) + + options[i] = []p2p.SwitchOption{ + p2p.WithReactor("MEMPOOL", reactor), + } + + reactors[i] = reactor + } + + // "Simulate" the networking layer + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + cfg := p2pTesting.TestingConfig{ + Count: n, + P2PCfg: pconfig, + SwitchOptions: options, + Channels: []byte{MempoolChannel}, } - p2p.MakeConnectedSwitches(pconfig, n, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("MEMPOOL", reactors[i]) - return s - }, p2p.Connect2Switches) + p2pTesting.MakeConnectedPeers(t, ctx, cfg) + return reactors } -func waitForTxsOnReactors(t *testing.T, txs types.Txs, reactors []*Reactor) { +func waitForTxsOnReactors( + t *testing.T, + txs types.Txs, + reactors []*Reactor, +) { t.Helper() - // wait for the txs in all mempools - wg := new(sync.WaitGroup) + ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelFn() + + // Wait for the txs to propagate in all mempools + var wg sync.WaitGroup + for i, reactor := range reactors { wg.Add(1) + go func(r *Reactor, reactorIndex int) { defer wg.Done() - waitForTxsOnReactor(t, txs, r, reactorIndex) + + reapedTxs := waitForTxsOnReactor(t, ctx, len(txs), r) + + for i, tx := range txs { + assert.Equalf(t, tx, reapedTxs[i], + fmt.Sprintf( + "txs at index %d on reactor %d don't match: %v vs %v", + i, reactorIndex, + tx, + reapedTxs[i], + ), + ) + } }(reactor, i) } - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - timer := time.After(timeout) - select { - case <-timer: - t.Fatal("Timed out waiting for txs") - case <-done: - } + wg.Wait() } -func waitForTxsOnReactor(t *testing.T, txs types.Txs, reactor *Reactor, reactorIndex int) { +func waitForTxsOnReactor( + t *testing.T, + ctx context.Context, + expectedLength int, + reactor *Reactor, +) types.Txs { t.Helper() - mempool := reactor.mempool - for mempool.Size() < len(txs) { - time.Sleep(time.Millisecond * 100) - } - - reapedTxs := mempool.ReapMaxTxs(len(txs)) - for i, tx := range txs { - assert.Equalf(t, tx, reapedTxs[i], - "txs at index %d on reactor %d don't match: %v vs %v", i, reactorIndex, tx, reapedTxs[i]) + var ( + mempool = reactor.mempool + ticker = time.NewTicker(100 * time.Millisecond) + ) + + for { + select { + case <-ctx.Done(): + t.Fatal("timed out waiting for txs") + case <-ticker.C: + if mempool.Size() < expectedLength { + continue + } + + return mempool.ReapMaxTxs(expectedLength) + } } } @@ -100,32 +153,29 @@ func ensureNoTxs(t *testing.T, reactor *Reactor, timeout time.Duration) { assert.Zero(t, reactor.mempool.Size()) } -const ( - numTxs = 1000 - timeout = 120 * time.Second // ridiculously high because CircleCI is slow -) - func TestReactorBroadcastTxMessage(t *testing.T) { t.Parallel() mconfig := memcfg.TestMempoolConfig() - pconfig := p2pcfg.TestP2PConfig() + pconfig := testP2PConfig() const N = 4 - reactors := makeAndConnectReactors(mconfig, pconfig, N) - defer func() { + reactors := makeAndConnectReactors(t, mconfig, pconfig, N) + t.Cleanup(func() { for _, r := range reactors { - r.Stop() + assert.NoError(t, r.Stop()) } - }() + }) + for _, r := range reactors { for _, peer := range r.Switch.Peers().List() { + fmt.Printf("Setting peer %s\n", peer.ID()) peer.Set(types.PeerStateKey, peerState{1}) } } // send a bunch of txs to the first reactor's mempool // and wait for them all to be received in the others - txs := checkTxs(t, reactors[0].mempool, numTxs, UnknownPeerID, true) + txs := checkTxs(t, reactors[0].mempool, 1000, UnknownPeerID, true) waitForTxsOnReactors(t, txs, reactors) } @@ -133,9 +183,9 @@ func TestReactorNoBroadcastToSender(t *testing.T) { t.Parallel() mconfig := memcfg.TestMempoolConfig() - pconfig := p2pcfg.TestP2PConfig() + pconfig := testP2PConfig() const N = 2 - reactors := makeAndConnectReactors(mconfig, pconfig, N) + reactors := makeAndConnectReactors(t, mconfig, pconfig, N) defer func() { for _, r := range reactors { r.Stop() @@ -144,7 +194,7 @@ func TestReactorNoBroadcastToSender(t *testing.T) { // send a bunch of txs to the first reactor's mempool, claiming it came from peer // ensure peer gets no txs - checkTxs(t, reactors[0].mempool, numTxs, 1, true) + checkTxs(t, reactors[0].mempool, 1000, 1, true) ensureNoTxs(t, reactors[1], 100*time.Millisecond) } @@ -158,9 +208,9 @@ func TestFlappyBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { } mconfig := memcfg.TestMempoolConfig() - pconfig := p2pcfg.TestP2PConfig() + pconfig := testP2PConfig() const N = 2 - reactors := makeAndConnectReactors(mconfig, pconfig, N) + reactors := makeAndConnectReactors(t, mconfig, pconfig, N) defer func() { for _, r := range reactors { r.Stop() @@ -186,9 +236,9 @@ func TestFlappyBroadcastTxForPeerStopsWhenReactorStops(t *testing.T) { } mconfig := memcfg.TestMempoolConfig() - pconfig := p2pcfg.TestP2PConfig() + pconfig := testP2PConfig() const N = 2 - reactors := makeAndConnectReactors(mconfig, pconfig, N) + reactors := makeAndConnectReactors(t, mconfig, pconfig, N) // stop reactors for _, r := range reactors { @@ -205,15 +255,15 @@ func TestMempoolIDsBasic(t *testing.T) { ids := newMempoolIDs() - peer := mock.NewPeer(net.IP{127, 0, 0, 1}) + id := p2pTypes.GenerateNodeKey().ID() - ids.ReserveForPeer(peer) - assert.EqualValues(t, 1, ids.GetForPeer(peer)) - ids.Reclaim(peer) + ids.ReserveForPeer(id) + assert.EqualValues(t, 1, ids.GetForPeer(id)) + ids.Reclaim(id) - ids.ReserveForPeer(peer) - assert.EqualValues(t, 2, ids.GetForPeer(peer)) - ids.Reclaim(peer) + ids.ReserveForPeer(id) + assert.EqualValues(t, 2, ids.GetForPeer(id)) + ids.Reclaim(id) } func TestMempoolIDsPanicsIfNodeRequestsOvermaxActiveIDs(t *testing.T) { @@ -227,12 +277,13 @@ func TestMempoolIDsPanicsIfNodeRequestsOvermaxActiveIDs(t *testing.T) { ids := newMempoolIDs() for i := 0; i < maxActiveIDs-1; i++ { - peer := mock.NewPeer(net.IP{127, 0, 0, 1}) - ids.ReserveForPeer(peer) + id := p2pTypes.GenerateNodeKey().ID() + ids.ReserveForPeer(id) } assert.Panics(t, func() { - peer := mock.NewPeer(net.IP{127, 0, 0, 1}) - ids.ReserveForPeer(peer) + id := p2pTypes.GenerateNodeKey().ID() + + ids.ReserveForPeer(id) }) } diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index e29de3dd1ae..3315668d267 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -12,12 +12,16 @@ import ( "sync" "time" + goErrors "errors" + "github.com/gnolang/gno/tm2/pkg/bft/appconn" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" + "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/discovery" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/rs/cors" "github.com/gnolang/gno/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" bc "github.com/gnolang/gno/tm2/pkg/bft/blockchain" cfg "github.com/gnolang/gno/tm2/pkg/bft/config" cs "github.com/gnolang/gno/tm2/pkg/bft/consensus" @@ -87,7 +91,7 @@ func DefaultNewNode( logger *slog.Logger, ) (*Node, error) { // Generate node PrivKey - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + nodeKey, err := p2pTypes.LoadOrGenNodeKey(config.NodeKeyFile()) if err != nil { return nil, err } @@ -118,30 +122,6 @@ func DefaultNewNode( // Option sets a parameter for the node. type Option func(*Node) -// CustomReactors allows you to add custom reactors (name -> p2p.Reactor) to -// the node's Switch. -// -// WARNING: using any name from the below list of the existing reactors will -// result in replacing it with the custom one. -// -// - MEMPOOL -// - BLOCKCHAIN -// - CONSENSUS -// - EVIDENCE -// - PEX -func CustomReactors(reactors map[string]p2p.Reactor) Option { - return func(n *Node) { - for name, reactor := range reactors { - if existingReactor := n.sw.Reactor(name); existingReactor != nil { - n.sw.Logger.Info("Replacing existing reactor with a custom one", - "name", name, "existing", existingReactor, "custom", reactor) - n.sw.RemoveReactor(name, existingReactor) - } - n.sw.AddReactor(name, reactor) - } - } -} - // ------------------------------------------------------------------------------ // Node is the highest level interface to a full Tendermint node. @@ -155,11 +135,12 @@ type Node struct { privValidator types.PrivValidator // local node's validator key // network - transport *p2p.MultiplexTransport - sw *p2p.Switch // p2p connections - nodeInfo p2p.NodeInfo - nodeKey *p2p.NodeKey // our node privkey - isListening bool + transport *p2p.MultiplexTransport + sw *p2p.MultiplexSwitch // p2p connections + discoveryReactor *discovery.Reactor // discovery reactor + nodeInfo p2pTypes.NodeInfo + nodeKey *p2pTypes.NodeKey // our node privkey + isListening bool // services evsw events.EventSwitch @@ -289,14 +270,21 @@ func createMempoolAndMempoolReactor(config *cfg.Config, proxyApp appconn.AppConn return mempoolReactor, mempool } -func createBlockchainReactor(config *cfg.Config, +func createBlockchainReactor( state sm.State, blockExec *sm.BlockExecutor, blockStore *store.BlockStore, fastSync bool, + switchToConsensusFn bc.SwitchToConsensusFn, logger *slog.Logger, ) (bcReactor p2p.Reactor, err error) { - bcReactor = bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor = bc.NewBlockchainReactor( + state.Copy(), + blockExec, + blockStore, + fastSync, + switchToConsensusFn, + ) bcReactor.SetLogger(logger.With("module", "blockchain")) return bcReactor, nil @@ -331,93 +319,15 @@ func createConsensusReactor(config *cfg.Config, return consensusReactor, consensusState } -func createTransport(config *cfg.Config, nodeInfo p2p.NodeInfo, nodeKey *p2p.NodeKey, proxyApp appconn.AppConns) (*p2p.MultiplexTransport, []p2p.PeerFilterFunc) { - var ( - mConnConfig = p2p.MConnConfig(config.P2P) - transport = p2p.NewMultiplexTransport(nodeInfo, *nodeKey, mConnConfig) - connFilters = []p2p.ConnFilterFunc{} - peerFilters = []p2p.PeerFilterFunc{} - ) - - if !config.P2P.AllowDuplicateIP { - connFilters = append(connFilters, p2p.ConnDuplicateIPFilter()) - } - - // Filter peers by addr or pubkey with an ABCI query. - // If the query return code is OK, add peer. - if config.FilterPeers { - connFilters = append( - connFilters, - // ABCI query for address filtering. - func(_ p2p.ConnSet, c net.Conn, _ []net.IP) error { - res, err := proxyApp.Query().QuerySync(abci.RequestQuery{ - Path: fmt.Sprintf("/p2p/filter/addr/%s", c.RemoteAddr().String()), - }) - if err != nil { - return err - } - if res.IsErr() { - return fmt.Errorf("error querying abci app: %v", res) - } - - return nil - }, - ) - - peerFilters = append( - peerFilters, - // ABCI query for ID filtering. - func(_ p2p.IPeerSet, p p2p.Peer) error { - res, err := proxyApp.Query().QuerySync(abci.RequestQuery{ - Path: fmt.Sprintf("/p2p/filter/id/%s", p.ID()), - }) - if err != nil { - return err - } - if res.IsErr() { - return fmt.Errorf("error querying abci app: %v", res) - } - - return nil - }, - ) - } - - p2p.MultiplexTransportConnFilters(connFilters...)(transport) - return transport, peerFilters -} - -func createSwitch(config *cfg.Config, - transport *p2p.MultiplexTransport, - peerFilters []p2p.PeerFilterFunc, - mempoolReactor *mempl.Reactor, - bcReactor p2p.Reactor, - consensusReactor *cs.ConsensusReactor, - nodeInfo p2p.NodeInfo, - nodeKey *p2p.NodeKey, - p2pLogger *slog.Logger, -) *p2p.Switch { - sw := p2p.NewSwitch( - config.P2P, - transport, - p2p.SwitchPeerFilters(peerFilters...), - ) - sw.SetLogger(p2pLogger) - sw.AddReactor("MEMPOOL", mempoolReactor) - sw.AddReactor("BLOCKCHAIN", bcReactor) - sw.AddReactor("CONSENSUS", consensusReactor) - - sw.SetNodeInfo(nodeInfo) - sw.SetNodeKey(nodeKey) - - p2pLogger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", config.NodeKeyFile()) - return sw +type nodeReactor struct { + name string + reactor p2p.Reactor } // NewNode returns a new, ready to go, Tendermint Node. func NewNode(config *cfg.Config, privValidator types.PrivValidator, - nodeKey *p2p.NodeKey, + nodeKey *p2pTypes.NodeKey, clientCreator appconn.ClientCreator, genesisDocProvider GenesisDocProvider, dbProvider DBProvider, @@ -506,38 +416,103 @@ func NewNode(config *cfg.Config, mempool, ) - // Make BlockchainReactor - bcReactor, err := createBlockchainReactor(config, state, blockExec, blockStore, fastSync, logger) - if err != nil { - return nil, errors.Wrap(err, "could not create blockchain reactor") - } - // Make ConsensusReactor consensusReactor, consensusState := createConsensusReactor( config, state, blockExec, blockStore, mempool, privValidator, fastSync, evsw, consensusLogger, ) + // Make BlockchainReactor + bcReactor, err := createBlockchainReactor( + state, + blockExec, + blockStore, + fastSync, + consensusReactor.SwitchToConsensus, + logger, + ) + if err != nil { + return nil, errors.Wrap(err, "could not create blockchain reactor") + } + + reactors := []nodeReactor{ + { + "MEMPOOL", mempoolReactor, + }, + { + "BLOCKCHAIN", bcReactor, + }, + { + "CONSENSUS", consensusReactor, + }, + } + nodeInfo, err := makeNodeInfo(config, nodeKey, txEventStore, genDoc, state) if err != nil { return nil, errors.Wrap(err, "error making NodeInfo") } - // Setup Transport. - transport, peerFilters := createTransport(config, nodeInfo, nodeKey, proxyApp) - - // Setup Switch. p2pLogger := logger.With("module", "p2p") - sw := createSwitch( - config, transport, peerFilters, mempoolReactor, bcReactor, - consensusReactor, nodeInfo, nodeKey, p2pLogger, + + // Setup the multiplex transport, used by the P2P switch + transport := p2p.NewMultiplexTransport( + nodeInfo, + *nodeKey, + conn.MConfigFromP2P(config.P2P), + p2pLogger.With("transport", "multiplex"), ) - err = sw.AddPersistentPeers(splitAndTrimEmpty(config.P2P.PersistentPeers, ",", " ")) - if err != nil { - return nil, errors.Wrap(err, "could not add peers from persistent_peers field") + var discoveryReactor *discovery.Reactor + + if config.P2P.PeerExchange { + discoveryReactor = discovery.NewReactor() + + discoveryReactor.SetLogger(logger.With("module", "discovery")) + + reactors = append(reactors, nodeReactor{ + name: "DISCOVERY", + reactor: discoveryReactor, + }) + } + + // Setup MultiplexSwitch. + peerAddrs, errs := p2pTypes.NewNetAddressFromStrings( + splitAndTrimEmpty(config.P2P.PersistentPeers, ",", " "), + ) + for _, err = range errs { + p2pLogger.Error("invalid persistent peer address", "err", err) + } + + // Parse the private peer IDs + privatePeerIDs, errs := p2pTypes.NewIDFromStrings( + splitAndTrimEmpty(config.P2P.PrivatePeerIDs, ",", " "), + ) + for _, err = range errs { + p2pLogger.Error("invalid private peer ID", "err", err) + } + + // Prepare the misc switch options + opts := []p2p.SwitchOption{ + p2p.WithPersistentPeers(peerAddrs), + p2p.WithPrivatePeers(privatePeerIDs), + p2p.WithMaxInboundPeers(config.P2P.MaxNumInboundPeers), + p2p.WithMaxOutboundPeers(config.P2P.MaxNumOutboundPeers), + } + + // Prepare the reactor switch options + for _, r := range reactors { + opts = append(opts, p2p.WithReactor(r.name, r.reactor)) } + sw := p2p.NewMultiplexSwitch( + transport, + opts..., + ) + + sw.SetLogger(p2pLogger) + + p2pLogger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", config.NodeKeyFile()) + if config.ProfListenAddress != "" { server := &http.Server{ Addr: config.ProfListenAddress, @@ -554,10 +529,11 @@ func NewNode(config *cfg.Config, genesisDoc: genDoc, privValidator: privValidator, - transport: transport, - sw: sw, - nodeInfo: nodeInfo, - nodeKey: nodeKey, + transport: transport, + sw: sw, + discoveryReactor: discoveryReactor, + nodeInfo: nodeInfo, + nodeKey: nodeKey, evsw: evsw, stateDB: stateDB, @@ -611,10 +587,16 @@ func (n *Node) OnStart() error { } // Start the transport. - addr, err := p2p.NewNetAddressFromString(p2p.NetAddressString(n.nodeKey.ID(), n.config.P2P.ListenAddress)) + lAddr := n.config.P2P.ExternalAddress + if lAddr == "" { + lAddr = n.config.P2P.ListenAddress + } + + addr, err := p2pTypes.NewNetAddressFromString(p2pTypes.NetAddressString(n.nodeKey.ID(), lAddr)) if err != nil { - return err + return fmt.Errorf("unable to parse network address, %w", err) } + if err := n.transport.Listen(*addr); err != nil { return err } @@ -639,11 +621,14 @@ func (n *Node) OnStart() error { } // Always connect to persistent peers - err = n.sw.DialPeersAsync(splitAndTrimEmpty(n.config.P2P.PersistentPeers, ",", " ")) - if err != nil { - return errors.Wrap(err, "could not dial peers from persistent_peers field") + peerAddrs, errs := p2pTypes.NewNetAddressFromStrings(splitAndTrimEmpty(n.config.P2P.PersistentPeers, ",", " ")) + for _, err := range errs { + n.Logger.Error("invalid persistent peer address", "err", err) } + // Dial the persistent peers + n.sw.DialPeers(peerAddrs...) + return nil } @@ -657,8 +642,15 @@ func (n *Node) OnStop() { n.evsw.Stop() n.eventStoreService.Stop() + // Stop the node p2p transport + if err := n.transport.Close(); err != nil { + n.Logger.Error("unable to gracefully close transport", "err", err) + } + // now stop the reactors - n.sw.Stop() + if err := n.sw.Stop(); err != nil { + n.Logger.Error("unable to gracefully close switch", "err", err) + } // stop mempool WAL if n.config.Mempool.WalEnabled() { @@ -791,7 +783,7 @@ func joinListenerAddresses(ll []net.Listener) string { } // Switch returns the Node's Switch. -func (n *Node) Switch() *p2p.Switch { +func (n *Node) Switch() *p2p.MultiplexSwitch { return n.sw } @@ -859,17 +851,17 @@ func (n *Node) IsListening() bool { } // NodeInfo returns the Node's Info from the Switch. -func (n *Node) NodeInfo() p2p.NodeInfo { +func (n *Node) NodeInfo() p2pTypes.NodeInfo { return n.nodeInfo } func makeNodeInfo( config *cfg.Config, - nodeKey *p2p.NodeKey, + nodeKey *p2pTypes.NodeKey, txEventStore eventstore.TxEventStore, genDoc *types.GenesisDoc, state sm.State, -) (p2p.NodeInfo, error) { +) (p2pTypes.NodeInfo, error) { txIndexerStatus := eventstore.StatusOff if txEventStore.GetType() != null.EventStoreType { txIndexerStatus = eventstore.StatusOn @@ -882,8 +874,9 @@ func makeNodeInfo( Version: state.AppVersion, }) - nodeInfo := p2p.NodeInfo{ + nodeInfo := p2pTypes.NodeInfo{ VersionSet: vset, + PeerID: nodeKey.ID(), Network: genDoc.ChainID, Version: version.Version, Channels: []byte{ @@ -892,24 +885,23 @@ func makeNodeInfo( mempl.MempoolChannel, }, Moniker: config.Moniker, - Other: p2p.NodeInfoOther{ + Other: p2pTypes.NodeInfoOther{ TxIndex: txIndexerStatus, RPCAddress: config.RPC.ListenAddress, }, } - lAddr := config.P2P.ExternalAddress - if lAddr == "" { - lAddr = config.P2P.ListenAddress + if config.P2P.PeerExchange { + nodeInfo.Channels = append(nodeInfo.Channels, discovery.Channel) } - addr, err := p2p.NewNetAddressFromString(p2p.NetAddressString(nodeKey.ID(), lAddr)) - if err != nil { - return nodeInfo, errors.Wrap(err, "invalid (local) node net address") + + // Validate the node info + err := nodeInfo.Validate() + if err != nil && !goErrors.Is(err, p2pTypes.ErrUnspecifiedIP) { + return p2pTypes.NodeInfo{}, fmt.Errorf("unable to validate node info, %w", err) } - nodeInfo.NetAddress = addr - err = nodeInfo.Validate() - return nodeInfo, err + return nodeInfo, nil } // ------------------------------------------------------------------------------ diff --git a/tm2/pkg/bft/node/node_test.go b/tm2/pkg/bft/node/node_test.go index 6e86a0bcc6f..1ea789d31c2 100644 --- a/tm2/pkg/bft/node/node_test.go +++ b/tm2/pkg/bft/node/node_test.go @@ -25,8 +25,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p" - p2pmock "github.com/gnolang/gno/tm2/pkg/p2p/mock" "github.com/gnolang/gno/tm2/pkg/random" ) @@ -40,8 +38,6 @@ func TestNodeStartStop(t *testing.T) { err = n.Start() require.NoError(t, err) - t.Logf("Started node %v", n.sw.NodeInfo()) - // wait for the node to produce a block blocksSub := events.SubscribeToEvent(n.EventSwitch(), "node_test", types.EventNewBlock{}) require.NoError(t, err) @@ -308,39 +304,6 @@ func TestCreateProposalBlock(t *testing.T) { assert.NoError(t, err) } -func TestNodeNewNodeCustomReactors(t *testing.T) { - config, genesisFile := cfg.ResetTestRoot("node_new_node_custom_reactors_test") - defer os.RemoveAll(config.RootDir) - - cr := p2pmock.NewReactor() - customBlockchainReactor := p2pmock.NewReactor() - - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) - require.NoError(t, err) - - n, err := NewNode(config, - privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()), - nodeKey, - proxy.DefaultClientCreator(nil, config.ProxyApp, config.ABCI, config.DBDir()), - DefaultGenesisDocProviderFunc(genesisFile), - DefaultDBProvider, - events.NewEventSwitch(), - log.NewTestingLogger(t), - CustomReactors(map[string]p2p.Reactor{"FOO": cr, "BLOCKCHAIN": customBlockchainReactor}), - ) - require.NoError(t, err) - - err = n.Start() - require.NoError(t, err) - defer n.Stop() - - assert.True(t, cr.IsRunning()) - assert.Equal(t, cr, n.Switch().Reactor("FOO")) - - assert.True(t, customBlockchainReactor.IsRunning()) - assert.Equal(t, customBlockchainReactor, n.Switch().Reactor("BLOCKCHAIN")) -} - func state(nVals int, height int64) (sm.State, dbm.DB) { vals := make([]types.GenesisValidator, nVals) for i := 0; i < nVals; i++ { diff --git a/tm2/pkg/bft/rpc/client/batch_test.go b/tm2/pkg/bft/rpc/client/batch_test.go index 52930e5c372..fcd0f3f834d 100644 --- a/tm2/pkg/bft/rpc/client/batch_test.go +++ b/tm2/pkg/bft/rpc/client/batch_test.go @@ -10,7 +10,7 @@ import ( ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -116,7 +116,7 @@ func TestRPCBatch_Send(t *testing.T) { var ( numRequests = 10 expectedStatus = &ctypes.ResultStatus{ - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, } @@ -160,7 +160,7 @@ func TestRPCBatch_Endpoints(t *testing.T) { { statusMethod, &ctypes.ResultStatus{ - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, }, diff --git a/tm2/pkg/bft/rpc/client/client_test.go b/tm2/pkg/bft/rpc/client/client_test.go index cb88c91fc5f..31889f59883 100644 --- a/tm2/pkg/bft/rpc/client/client_test.go +++ b/tm2/pkg/bft/rpc/client/client_test.go @@ -14,7 +14,7 @@ import ( ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -114,7 +114,7 @@ func TestRPCClient_Status(t *testing.T) { var ( expectedStatus = &ctypes.ResultStatus{ - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, } @@ -811,17 +811,17 @@ func TestRPCClient_Batch(t *testing.T) { var ( expectedStatuses = []*ctypes.ResultStatus{ { - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, }, { - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, }, { - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, }, diff --git a/tm2/pkg/bft/rpc/client/e2e_test.go b/tm2/pkg/bft/rpc/client/e2e_test.go index 08d4b9b735d..358c66b0b26 100644 --- a/tm2/pkg/bft/rpc/client/e2e_test.go +++ b/tm2/pkg/bft/rpc/client/e2e_test.go @@ -13,7 +13,7 @@ import ( ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -170,7 +170,7 @@ func TestRPCClient_E2E_Endpoints(t *testing.T) { { statusMethod, &ctypes.ResultStatus{ - NodeInfo: p2p.NodeInfo{ + NodeInfo: p2pTypes.NodeInfo{ Moniker: "dummy", }, }, diff --git a/tm2/pkg/bft/rpc/client/local.go b/tm2/pkg/bft/rpc/client/local.go index 59c4216a468..4bc724e7d70 100644 --- a/tm2/pkg/bft/rpc/client/local.go +++ b/tm2/pkg/bft/rpc/client/local.go @@ -106,14 +106,6 @@ func (c *Local) Health() (*ctypes.ResultHealth, error) { return core.Health(c.ctx) } -func (c *Local) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { - return core.UnsafeDialSeeds(c.ctx, seeds) -} - -func (c *Local) DialPeers(peers []string, persistent bool) (*ctypes.ResultDialPeers, error) { - return core.UnsafeDialPeers(c.ctx, peers, persistent) -} - func (c *Local) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { return core.BlockchainInfo(c.ctx, minHeight, maxHeight) } diff --git a/tm2/pkg/bft/rpc/core/net.go b/tm2/pkg/bft/rpc/core/net.go index 975d5ed822f..f8839b7d91f 100644 --- a/tm2/pkg/bft/rpc/core/net.go +++ b/tm2/pkg/bft/rpc/core/net.go @@ -3,7 +3,6 @@ package core import ( ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/errors" ) // Get network info. @@ -154,10 +153,14 @@ import ( // } // // ``` -func NetInfo(ctx *rpctypes.Context) (*ctypes.ResultNetInfo, error) { - out, in, _ := p2pPeers.NumPeers() +func NetInfo(_ *rpctypes.Context) (*ctypes.ResultNetInfo, error) { + var ( + set = p2pPeers.Peers() + out, in = set.NumOutbound(), set.NumInbound() + ) + peers := make([]ctypes.Peer, 0, out+in) - for _, peer := range p2pPeers.Peers().List() { + for _, peer := range set.List() { nodeInfo := peer.NodeInfo() peers = append(peers, ctypes.Peer{ NodeInfo: nodeInfo, @@ -166,9 +169,7 @@ func NetInfo(ctx *rpctypes.Context) (*ctypes.ResultNetInfo, error) { RemoteIP: peer.RemoteIP().String(), }) } - // TODO: Should we include PersistentPeers and Seeds in here? - // PRO: useful info - // CON: privacy + return &ctypes.ResultNetInfo{ Listening: p2pTransport.IsListening(), Listeners: p2pTransport.Listeners(), @@ -177,33 +178,6 @@ func NetInfo(ctx *rpctypes.Context) (*ctypes.ResultNetInfo, error) { }, nil } -func UnsafeDialSeeds(ctx *rpctypes.Context, seeds []string) (*ctypes.ResultDialSeeds, error) { - if len(seeds) == 0 { - return &ctypes.ResultDialSeeds{}, errors.New("No seeds provided") - } - logger.Info("DialSeeds", "seeds", seeds) - if err := p2pPeers.DialPeersAsync(seeds); err != nil { - return &ctypes.ResultDialSeeds{}, err - } - return &ctypes.ResultDialSeeds{Log: "Dialing seeds in progress. See /net_info for details"}, nil -} - -func UnsafeDialPeers(ctx *rpctypes.Context, peers []string, persistent bool) (*ctypes.ResultDialPeers, error) { - if len(peers) == 0 { - return &ctypes.ResultDialPeers{}, errors.New("No peers provided") - } - logger.Info("DialPeers", "peers", peers, "persistent", persistent) - if persistent { - if err := p2pPeers.AddPersistentPeers(peers); err != nil { - return &ctypes.ResultDialPeers{}, err - } - } - if err := p2pPeers.DialPeersAsync(peers); err != nil { - return &ctypes.ResultDialPeers{}, err - } - return &ctypes.ResultDialPeers{Log: "Dialing peers in progress. See /net_info for details"}, nil -} - // Get genesis file. // // ```shell diff --git a/tm2/pkg/bft/rpc/core/net_test.go b/tm2/pkg/bft/rpc/core/net_test.go deleted file mode 100644 index 3273837b6ce..00000000000 --- a/tm2/pkg/bft/rpc/core/net_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package core - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p" - p2pcfg "github.com/gnolang/gno/tm2/pkg/p2p/config" -) - -func TestUnsafeDialSeeds(t *testing.T) { - t.Parallel() - - sw := p2p.MakeSwitch(p2pcfg.DefaultP2PConfig(), 1, "testing", "123.123.123", - func(n int, sw *p2p.Switch) *p2p.Switch { return sw }) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() - - logger = log.NewNoopLogger() - p2pPeers = sw - - testCases := []struct { - seeds []string - isErr bool - }{ - {[]string{}, true}, - {[]string{"g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:41198"}, false}, - {[]string{"127.0.0.1:41198"}, true}, - } - - for _, tc := range testCases { - res, err := UnsafeDialSeeds(&rpctypes.Context{}, tc.seeds) - if tc.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.NotNil(t, res) - } - } -} - -func TestUnsafeDialPeers(t *testing.T) { - t.Parallel() - - sw := p2p.MakeSwitch(p2pcfg.DefaultP2PConfig(), 1, "testing", "123.123.123", - func(n int, sw *p2p.Switch) *p2p.Switch { return sw }) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() - - logger = log.NewNoopLogger() - p2pPeers = sw - - testCases := []struct { - peers []string - isErr bool - }{ - {[]string{}, true}, - {[]string{"g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:41198"}, false}, - {[]string{"127.0.0.1:41198"}, true}, - } - - for _, tc := range testCases { - res, err := UnsafeDialPeers(&rpctypes.Context{}, tc.peers, false) - if tc.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.NotNil(t, res) - } - } -} diff --git a/tm2/pkg/bft/rpc/core/pipe.go b/tm2/pkg/bft/rpc/core/pipe.go index 9493e7c5873..085fc35da55 100644 --- a/tm2/pkg/bft/rpc/core/pipe.go +++ b/tm2/pkg/bft/rpc/core/pipe.go @@ -15,6 +15,7 @@ import ( dbm "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" ) const ( @@ -38,14 +39,11 @@ type Consensus interface { type transport interface { Listeners() []string IsListening() bool - NodeInfo() p2p.NodeInfo + NodeInfo() p2pTypes.NodeInfo } type peers interface { - AddPersistentPeers([]string) error - DialPeersAsync([]string) error - NumPeers() (outbound, inbound, dialig int) - Peers() p2p.IPeerSet + Peers() p2p.PeerSet } // ---------------------------------------------- diff --git a/tm2/pkg/bft/rpc/core/routes.go b/tm2/pkg/bft/rpc/core/routes.go index 8d210f67985..76217a7cbd9 100644 --- a/tm2/pkg/bft/rpc/core/routes.go +++ b/tm2/pkg/bft/rpc/core/routes.go @@ -36,8 +36,6 @@ var Routes = map[string]*rpc.RPCFunc{ func AddUnsafeRoutes() { // control API - Routes["dial_seeds"] = rpc.NewRPCFunc(UnsafeDialSeeds, "seeds") - Routes["dial_peers"] = rpc.NewRPCFunc(UnsafeDialPeers, "peers,persistent") Routes["unsafe_flush_mempool"] = rpc.NewRPCFunc(UnsafeFlushMempool, "") // profiler API diff --git a/tm2/pkg/bft/rpc/core/types/responses.go b/tm2/pkg/bft/rpc/core/types/responses.go index 2874517147d..76474867b27 100644 --- a/tm2/pkg/bft/rpc/core/types/responses.go +++ b/tm2/pkg/bft/rpc/core/types/responses.go @@ -11,6 +11,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/p2p" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" ) // List of blocks @@ -74,9 +75,9 @@ type ValidatorInfo struct { // Node Status type ResultStatus struct { - NodeInfo p2p.NodeInfo `json:"node_info"` - SyncInfo SyncInfo `json:"sync_info"` - ValidatorInfo ValidatorInfo `json:"validator_info"` + NodeInfo p2pTypes.NodeInfo `json:"node_info"` + SyncInfo SyncInfo `json:"sync_info"` + ValidatorInfo ValidatorInfo `json:"validator_info"` } // Is TxIndexing enabled @@ -107,7 +108,7 @@ type ResultDialPeers struct { // A peer type Peer struct { - NodeInfo p2p.NodeInfo `json:"node_info"` + NodeInfo p2pTypes.NodeInfo `json:"node_info"` IsOutbound bool `json:"is_outbound"` ConnectionStatus p2p.ConnectionStatus `json:"connection_status"` RemoteIP string `json:"remote_ip"` diff --git a/tm2/pkg/bft/rpc/core/types/responses_test.go b/tm2/pkg/bft/rpc/core/types/responses_test.go index 268a8d25c34..7d03addc546 100644 --- a/tm2/pkg/bft/rpc/core/types/responses_test.go +++ b/tm2/pkg/bft/rpc/core/types/responses_test.go @@ -3,9 +3,8 @@ package core_types import ( "testing" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" - - "github.com/gnolang/gno/tm2/pkg/p2p" ) func TestStatusIndexer(t *testing.T) { @@ -17,17 +16,17 @@ func TestStatusIndexer(t *testing.T) { status = &ResultStatus{} assert.False(t, status.TxIndexEnabled()) - status.NodeInfo = p2p.NodeInfo{} + status.NodeInfo = types.NodeInfo{} assert.False(t, status.TxIndexEnabled()) cases := []struct { expected bool - other p2p.NodeInfoOther + other types.NodeInfoOther }{ - {false, p2p.NodeInfoOther{}}, - {false, p2p.NodeInfoOther{TxIndex: "aa"}}, - {false, p2p.NodeInfoOther{TxIndex: "off"}}, - {true, p2p.NodeInfoOther{TxIndex: "on"}}, + {false, types.NodeInfoOther{}}, + {false, types.NodeInfoOther{TxIndex: "aa"}}, + {false, types.NodeInfoOther{TxIndex: "off"}}, + {true, types.NodeInfoOther{TxIndex: "on"}}, } for _, tc := range cases { diff --git a/tm2/pkg/crypto/crypto.go b/tm2/pkg/crypto/crypto.go index 7757b75354e..7908a082d3b 100644 --- a/tm2/pkg/crypto/crypto.go +++ b/tm2/pkg/crypto/crypto.go @@ -3,6 +3,7 @@ package crypto import ( "bytes" "encoding/json" + "errors" "fmt" "github.com/gnolang/gno/tm2/pkg/bech32" @@ -128,6 +129,8 @@ func (addr *Address) DecodeString(str string) error { // ---------------------------------------- // ID +var ErrZeroID = errors.New("address ID is zero") + // The bech32 representation w/ bech32 prefix. type ID string @@ -141,16 +144,12 @@ func (id ID) String() string { func (id ID) Validate() error { if id.IsZero() { - return fmt.Errorf("zero ID is invalid") + return ErrZeroID } + var addr Address - err := addr.DecodeID(id) - return err -} -func AddressFromID(id ID) (addr Address, err error) { - err = addr.DecodeString(string(id)) - return + return addr.DecodeID(id) } func (addr Address) ID() ID { diff --git a/tm2/pkg/crypto/ed25519/ed25519.go b/tm2/pkg/crypto/ed25519/ed25519.go index 8976994986c..f8b9529b788 100644 --- a/tm2/pkg/crypto/ed25519/ed25519.go +++ b/tm2/pkg/crypto/ed25519/ed25519.go @@ -68,11 +68,9 @@ func (privKey PrivKeyEd25519) PubKey() crypto.PubKey { // Equals - you probably don't need to use this. // Runs in constant time based on length of the keys. func (privKey PrivKeyEd25519) Equals(other crypto.PrivKey) bool { - if otherEd, ok := other.(PrivKeyEd25519); ok { - return subtle.ConstantTimeCompare(privKey[:], otherEd[:]) == 1 - } else { - return false - } + otherEd, ok := other.(PrivKeyEd25519) + + return ok && subtle.ConstantTimeCompare(privKey[:], otherEd[:]) == 1 } // GenPrivKey generates a new ed25519 private key. diff --git a/tm2/pkg/internal/p2p/p2p.go b/tm2/pkg/internal/p2p/p2p.go new file mode 100644 index 00000000000..665c5bf7099 --- /dev/null +++ b/tm2/pkg/internal/p2p/p2p.go @@ -0,0 +1,251 @@ +// Package p2p contains testing code that is moved over, and adapted from p2p/test_utils.go. +// This isn't a good way to simulate the networking layer in TM2 modules. +// It actually isn't a good way to simulate the networking layer, in anything. +// +// Code is carried over to keep the testing code of p2p-dependent modules happy +// and "working". We should delete this entire package the second TM2 module unit tests don't +// need to rely on a live p2p cluster to pass. +package p2p + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "net" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/p2p" + p2pcfg "github.com/gnolang/gno/tm2/pkg/p2p/config" + "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/events" + p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/gnolang/gno/tm2/pkg/service" + "github.com/gnolang/gno/tm2/pkg/versionset" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" +) + +// TestingConfig is the P2P cluster testing config +type TestingConfig struct { + P2PCfg *p2pcfg.P2PConfig // the common p2p configuration + Count int // the size of the cluster + SwitchOptions map[int][]p2p.SwitchOption // multiplex switch options + Channels []byte // the common p2p peer multiplex channels +} + +// MakeConnectedPeers creates a cluster of peers, with the given options. +// Used to simulate the networking layer for a TM2 module +func MakeConnectedPeers( + t *testing.T, + ctx context.Context, + cfg TestingConfig, +) ([]*p2p.MultiplexSwitch, []*p2p.MultiplexTransport) { + t.Helper() + + // Initialize collections for switches, transports, and addresses. + var ( + sws = make([]*p2p.MultiplexSwitch, 0, cfg.Count) + ts = make([]*p2p.MultiplexTransport, 0, cfg.Count) + addrs = make([]*p2pTypes.NetAddress, 0, cfg.Count) + ) + + createTransport := func(index int) *p2p.MultiplexTransport { + // Generate a fresh key + key := p2pTypes.GenerateNodeKey() + + addr, err := p2pTypes.NewNetAddress( + key.ID(), + &net.TCPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 0, // random free port + }, + ) + require.NoError(t, err) + + info := p2pTypes.NodeInfo{ + VersionSet: versionset.VersionSet{ + versionset.VersionInfo{Name: "p2p", Version: "v0.0.0"}, + }, + PeerID: key.ID(), + Network: "testing", + Software: "p2ptest", + Version: "v1.2.3-rc.0-deadbeef", + Channels: cfg.Channels, + Moniker: fmt.Sprintf("node-%d", index), + Other: p2pTypes.NodeInfoOther{ + TxIndex: "off", + RPCAddress: fmt.Sprintf("127.0.0.1:%d", 0), + }, + } + + transport := p2p.NewMultiplexTransport( + info, + *key, + conn.MConfigFromP2P(cfg.P2PCfg), + log.NewNoopLogger(), + ) + + require.NoError(t, transport.Listen(*addr)) + t.Cleanup(func() { assert.NoError(t, transport.Close()) }) + + return transport + } + + // Create transports and gather addresses + for i := 0; i < cfg.Count; i++ { + transport := createTransport(i) + addr := transport.NetAddress() + + addrs = append(addrs, &addr) + ts = append(ts, transport) + } + + // Connect switches and ensure all peers are connected + connectPeers := func(switchIndex int) error { + multiplexSwitch := p2p.NewMultiplexSwitch( + ts[switchIndex], + cfg.SwitchOptions[switchIndex]..., + ) + + ch, unsubFn := multiplexSwitch.Subscribe(func(event events.Event) bool { + return event.Type() == events.PeerConnected + }) + defer unsubFn() + + // Start the switch + require.NoError(t, multiplexSwitch.Start()) + + // Save it + sws = append(sws, multiplexSwitch) + + if cfg.Count == 1 { + // No peers to dial, switch is alone + return nil + } + + // Async dial the other peers + multiplexSwitch.DialPeers(addrs...) + + // Set up an exit timer + timer := time.NewTimer(5 * time.Second) + defer timer.Stop() + + connectedPeers := make(map[p2pTypes.ID]struct{}) + + for { + select { + case evRaw := <-ch: + ev := evRaw.(events.PeerConnectedEvent) + + connectedPeers[ev.PeerID] = struct{}{} + + if len(connectedPeers) == cfg.Count-1 { + return nil + } + case <-timer.C: + return errors.New("timed out waiting for peer switches to connect") + } + } + } + + g, _ := errgroup.WithContext(ctx) + for i := 0; i < cfg.Count; i++ { + g.Go(func() error { return connectPeers(i) }) + } + + require.NoError(t, g.Wait()) + + return sws, ts +} + +// createRoutableAddr generates a valid, routable NetAddress for the given node ID using a secure random IP +func createRoutableAddr(t *testing.T, id p2pTypes.ID) *p2pTypes.NetAddress { + t.Helper() + + generateIP := func() string { + ip := make([]byte, 4) + + _, err := rand.Read(ip) + require.NoError(t, err) + + return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]) + } + + for { + addrStr := fmt.Sprintf("%s@%s:26656", id, generateIP()) + + netAddr, err := p2pTypes.NewNetAddressFromString(addrStr) + require.NoError(t, err) + + if netAddr.Routable() { + return netAddr + } + } +} + +// Peer is a live peer, utilized for testing purposes +type Peer struct { + *service.BaseService + ip net.IP + id p2pTypes.ID + addr *p2pTypes.NetAddress + kv map[string]any + + Outbound, Persistent, Private bool +} + +// NewPeer creates and starts a new mock peer. +// It generates a new routable address for the peer +func NewPeer(t *testing.T) *Peer { + t.Helper() + + var ( + nodeKey = p2pTypes.GenerateNodeKey() + netAddr = createRoutableAddr(t, nodeKey.ID()) + ) + + mp := &Peer{ + ip: netAddr.IP, + id: nodeKey.ID(), + addr: netAddr, + kv: make(map[string]any), + } + + mp.BaseService = service.NewBaseService(nil, "MockPeer", mp) + + require.NoError(t, mp.Start()) + + return mp +} + +func (mp *Peer) FlushStop() { mp.Stop() } +func (mp *Peer) TrySend(_ byte, _ []byte) bool { return true } +func (mp *Peer) Send(_ byte, _ []byte) bool { return true } +func (mp *Peer) NodeInfo() p2pTypes.NodeInfo { + return p2pTypes.NodeInfo{ + PeerID: mp.id, + } +} +func (mp *Peer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } +func (mp *Peer) ID() p2pTypes.ID { return mp.id } +func (mp *Peer) IsOutbound() bool { return mp.Outbound } +func (mp *Peer) IsPersistent() bool { return mp.Persistent } +func (mp *Peer) IsPrivate() bool { return mp.Private } +func (mp *Peer) Get(key string) interface{} { + if value, ok := mp.kv[key]; ok { + return value + } + return nil +} + +func (mp *Peer) Set(key string, value interface{}) { + mp.kv[key] = value +} +func (mp *Peer) RemoteIP() net.IP { return mp.ip } +func (mp *Peer) SocketAddr() *p2pTypes.NetAddress { return mp.addr } +func (mp *Peer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: mp.ip, Port: 8800} } +func (mp *Peer) CloseConn() error { return nil } diff --git a/tm2/pkg/p2p/README.md b/tm2/pkg/p2p/README.md index 81888403e1c..f64fafb53e0 100644 --- a/tm2/pkg/p2p/README.md +++ b/tm2/pkg/p2p/README.md @@ -1,4 +1,604 @@ -# p2p +## Overview -The p2p package provides an abstraction around peer-to-peer communication. +The `p2p` package, and its “sub-packages” contain the required building blocks for Tendermint2’s networking layer. +This document aims to explain the `p2p` terminology, and better document the way the `p2p` module works within the TM2 +ecosystem, especially in relation to other modules like `consensus`, `blockchain` and `mempool`. + +## Common Types + +To fully understand the `Concepts` section of the `p2p` documentation, there must be at least a basic understanding of +the terminology of the `p2p` module, because there are types that keep popping up constantly, and it’s worth +understanding what they’re about. + +### `NetAddress` + +```go +package types + +// NetAddress defines information about a peer on the network +// including its ID, IP address, and port +type NetAddress struct { + ID ID `json:"id"` // unique peer identifier (public key address) + IP net.IP `json:"ip"` // the IP part of the dial address + Port uint16 `json:"port"` // the port part of the dial address +} +``` + +A `NetAddress` is simply a wrapper for a unique peer in the network. + +This address consists of several parts: + +- the peer’s ID, derived from the peer’s public key (it’s the address). +- the peer’s dial address, used for executing TCP dials. + +### `ID` + +```go +// ID represents the cryptographically unique Peer ID +type ID = crypto.ID +``` + +The peer ID is the unique peer identifier. It is used for unambiguously resolving who a peer is, during communication. + +The reason the peer ID is utilized is because it is derived from the peer’s public key, used to encrypt communication, +and it needs to match the public key used in p2p communication. It can, and should be, considered unique. + +### `Reactor` + +Without going too much into detail in the terminology section, as a much more detailed explanation is discussed below: + +A `Reactor` is an abstraction of a Tendermint2 module, that needs to utilize the `p2p` layer. + +Currently active reactors in TM2, that utilize the p2p layer: + +- the consensus reactor, that handles consensus message passing +- the blockchain reactor, that handles block syncing +- the mempool reactor, that handles transaction gossiping + +All of these functionalities require a live p2p network to work, and `Reactor`s are the answer for how they can be aware +of things happening in the network (like new peers joining, for example). + +## Concepts + +### Peer + +`Peer` is an abstraction over a p2p connection that is: + +- **verified**, meaning it went through the handshaking process and the information the other peer shared checked out ( + this process is discussed in detail later). +- **multiplexed over TCP** (the only kind of p2p connections TM2 supports). + +```go +package p2p + +// Peer is a wrapper for a connected peer +type Peer interface { + service.Service + + FlushStop() + + ID() types.ID // peer's cryptographic ID + RemoteIP() net.IP // remote IP of the connection + RemoteAddr() net.Addr // remote address of the connection + + IsOutbound() bool // did we dial the peer + IsPersistent() bool // do we redial this peer when we disconnect + IsPrivate() bool // do we share the peer + + CloseConn() error // close original connection + + NodeInfo() types.NodeInfo // peer's info + Status() ConnectionStatus + SocketAddr() *types.NetAddress // actual address of the socket + + Send(byte, []byte) bool + TrySend(byte, []byte) bool + + Set(string, any) + Get(string) any +} +``` + +There are more than a few things to break down here, so let’s tackle them individually. + +The `Peer` abstraction holds callbacks relating to information about the actual live peer connection, such as what kind +of direction it is, what is the connection status, and others. + +```go +package p2p + +type Peer interface { + // ... + + ID() types.ID // peer's cryptographic ID + RemoteIP() net.IP // remote IP of the connection + RemoteAddr() net.Addr // remote address of the connection + + NodeInfo() types.NodeInfo // peer's info + Status() ConnectionStatus + SocketAddr() *types.NetAddress // actual address of the socket + + IsOutbound() bool // did we dial the peer + IsPersistent() bool // do we redial this peer when we disconnect + IsPrivate() bool // do we share the peer + + // ... +} + +``` + +However, there is part of the `Peer` abstraction that outlines the flipped design of the entire `p2p` module, and a +severe limitation of this implementation. + +```go +package p2p + +type Peer interface { + // ... + + Send(byte, []byte) bool + TrySend(byte, []byte) bool + + // ... +} +``` + +The `Peer` abstraction is used internally in `p2p`, but also by other modules that need to interact with the networking +layer — this is in itself the biggest crux of the current `p2p` implementation: modules *need to understand* how to use +and communicate with peers, regardless of the protocol logic. Networking is not an abstraction for the modules, but a +spec requirement. What this essentially means is there is heavy implementation leaking to parts of the TM2 codebase that +shouldn’t need to know how to handle individual peer broadcasts, or how to trigger custom protocol communication (like +syncing for example). +If `module A` wants to broadcast something to the peer network of the node, it needs to do something like this: + +```go +package main + +func main() { + // ... + + peers := sw.Peers().List() // fetch the peer list + + for _, p := range peers { + p.Send(...) // directly message the peer (imitate broadcast) + } + + // ... +} +``` + +An additional odd choice in the `Peer` API is the ability to use the peer as a KV store: + +```go +package p2p + +type Peer interface { + // ... + + Set(string, any) + Get(string) any + + // ... +} +``` + +For example, these methods are used within the `consensus` and `mempool` modules to keep track of active peer states ( +like current HRS data, or current peer mempool metadata). Instead of the module handling individual peer state, this +responsibility is shifted to the peer implementation, causing an odd code dependency situation. + +The root of this “flipped” design (modules needing to understand how to interact with peers) stems from the fact that +peers are instantiated with a multiplex TCP connection under the hood, and basically just wrap that connection. The +`Peer` API is an abstraction for the multiplexed TCP connection, under the hood. + +Changing this dependency stew would require a much larger rewrite of not just the `p2p` module, but other modules ( +`consensus`, `blockchain`, `mempool`) as well, and is as such left as-is. + +### Switch + +In short, a `Switch` is just the middleware layer that handles module <> `Transport` requests, and manages peers on a +high application level (that the `Transport` doesn’t concern itself with). + +The `Switch` is the entity that manages active peer connections. + +```go +package p2p + +// Switch is the abstraction in the p2p module that handles +// and manages peer connections thorough a Transport +type Switch interface { + // Broadcast publishes data on the given channel, to all peers + Broadcast(chID byte, data []byte) + + // Peers returns the latest peer set + Peers() PeerSet + + // Subscribe subscribes to active switch events + Subscribe(filterFn events.EventFilter) (<-chan events.Event, func()) + + // StopPeerForError stops the peer with the given reason + StopPeerForError(peer Peer, err error) + + // DialPeers marks the given peers as ready for async dialing + DialPeers(peerAddrs ...*types.NetAddress) +} + +``` + +The API of the `Switch` is relatively straightforward. Users of the `Switch` instantiate it with a `Transport`, and +utilize it as-is. + +The API of the `Switch` is geared towards asynchronicity, and as such users of the `Switch` need to adapt to some +limitations, such as not having synchronous dials, or synchronous broadcasts. + +#### Services + +There are 3 services that run on top of the `MultiplexSwitch`, upon startup: + +- **the accept service** +- **the dial service** +- **the redial service** + +```go +package p2p + +// OnStart implements BaseService. It starts all the reactors and peers. +func (sw *MultiplexSwitch) OnStart() error { + // Start reactors + for _, reactor := range sw.reactors { + if err := reactor.Start(); err != nil { + return fmt.Errorf("unable to start reactor %w", err) + } + } + + // Run the peer accept routine. + // The accept routine asynchronously accepts + // and processes incoming peer connections + go sw.runAcceptLoop(sw.ctx) + + // Run the dial routine. + // The dial routine parses items in the dial queue + // and initiates outbound peer connections + go sw.runDialLoop(sw.ctx) + + // Run the redial routine. + // The redial routine monitors for important + // peer disconnects, and attempts to reconnect + // to them + go sw.runRedialLoop(sw.ctx) + + return nil +} +``` + +##### Accept Service + +The `MultiplexSwitch` needs to actively listen for incoming connections, and handle them accordingly. These situations +occur when a peer *Dials* (more on this later) another peer, and wants to establish a connection. This connection is +outbound for one peer, and inbound for the other. + +Depending on what kind of security policies or configuration the peer has in place, the connection can be accepted, or +rejected for a number of reasons: + +- the maximum number of inbound peers is reached +- the multiplex connection fails upon startup (rare) + +The `Switch` relies on the `Transport` to return a **verified and valid** peer connection. After the `Transport` +delivers, the `Switch` makes sure having the peer makes sense, given the p2p configuration of the node. + +```go +package p2p + +func (sw *MultiplexSwitch) runAcceptLoop(ctx context.Context) { + // ... + + p, err := sw.transport.Accept(ctx, sw.peerBehavior) + if err != nil { + sw.Logger.Error( + "error encountered during peer connection accept", + "err", err, + ) + + continue + } + + // Ignore connection if we already have enough peers. + if in := sw.Peers().NumInbound(); in >= sw.maxInboundPeers { + sw.Logger.Info( + "Ignoring inbound connection: already have enough inbound peers", + "address", p.SocketAddr(), + "have", in, + "max", sw.maxInboundPeers, + ) + + sw.transport.Remove(p) + + continue + } + + // ... +} + +``` + +In fact, this is the central point in the relationship between the `Switch` and `Transport`. +The `Transport` is responsible for establishing the connection, and the `Switch` is responsible for handling it after +it’s been established. + +When TM2 modules communicate with the `p2p` module, they communicate *with the `Switch`, not the `Transport`* to execute +peer-related actions. + +##### Dial Service + +Peers are dialed asynchronously in the `Switch`, as is suggested by the `Switch` API: + +```go +DialPeers(peerAddrs ...*types.NetAddress) +``` + +The `MultiplexSwitch` implementation utilizes a concept called a *dial queue*. + +A dial queue is a priority-based queue (sorted by dial time, ascending) from which dial requests are taken out of and +executed in the form of peer dialing (through the `Transport`, of course). + +The queue needs to be sorted by the dial time, since there are asynchronous dial requests that need to be executed as +soon as possible, while others can wait to be executed up until a certain point in time. + +```go +package p2p + +func (sw *MultiplexSwitch) runDialLoop(ctx context.Context) { + // ... + + // Grab a dial item + item := sw.dialQueue.Peek() + if item == nil { + // Nothing to dial + continue + } + + // Check if the dial time is right + // for the item + if time.Now().Before(item.Time) { + // Nothing to dial + continue + } + + // Pop the item from the dial queue + item = sw.dialQueue.Pop() + + // Dial the peer + sw.Logger.Info( + "dialing peer", + "address", item.Address.String(), + ) + + // ... +} +``` + +To follow the outcomes of dial requests, users of the `Switch` can subscribe to peer events (more on this later). + +##### Redial Service + +The TM2 `p2p` module has a concept of something called *persistent peers*. + +Persistent peers are specific peers whose connections must be preserved, at all costs. They are specified in the +top-level node P2P configuration, under `p2p.persistent_peers`. + +These peer connections are special, as they don’t adhere to high-level configuration limits like the maximum peer cap, +instead, they are monitored and handled actively. + +A good candidate for a persistent peer is a bootnode, that bootstraps and facilitates peer discovery for the network. + +If a persistent peer connection is lost for whatever reason (for ex, the peer disconnects), the redial service of the +`MultiplexSwitch` will create a dial request for the dial service, and attempt to re-establish the lost connection. + +```go +package p2p + +func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { + // ... + + var ( + peers = sw.Peers() + peersToDial = make([]*types.NetAddress, 0) + ) + + sw.persistentPeers.Range(func(key, value any) bool { + var ( + id = key.(types.ID) + addr = value.(*types.NetAddress) + ) + + // Check if the peer is part of the peer set + // or is scheduled for dialing + if peers.Has(id) || sw.dialQueue.Has(addr) { + return true + } + + peersToDial = append(peersToDial, addr) + + return true + }) + + if len(peersToDial) == 0 { + // No persistent peers are missing + return + } + + // Add the peers to the dial queue + sw.DialPeers(peersToDial...) + + // ... +} +``` + +#### Events + +The `Switch` is meant to be asynchronous. + +This means that processes like dialing peers, removing peers, doing broadcasts and more, is not a synchronous blocking +process for the `Switch` user. + +To be able to tap into the outcome of these asynchronous events, the `Switch` utilizes a simple event system, based on +event filters. + +```go +package main + +func main() { + // ... + + // Subscribe to live switch events + ch, unsubFn := multiplexSwitch.Subscribe(func(event events.Event) bool { + // This subscription will only return "PeerConnected" events + return event.Type() == events.PeerConnected + }) + + defer unsubFn() // removes the subscription + + select { + // Events are sent to the channel as soon as + // they appear and pass the subscription filter + case ev <- ch: + e := ev.(*events.PeerConnectedEvent) + // use event data... + case <-ctx.Done(): + // ... + } + + // ... +} +``` + +An event setup like this is useful for example when the user of the `Switch` wants to capture successful peer dial +events, in realtime. + +#### What is “peer behavior”? + +```go +package p2p + +// PeerBehavior wraps the Reactor and MultiplexSwitch information a Transport would need when +// dialing or accepting new Peer connections. +// It is worth noting that the only reason why this information is required in the first place, +// is because Peers expose an API through which different TM modules can interact with them. +// In the future™, modules should not directly "Send" anything to Peers, but instead communicate through +// other mediums, such as the P2P module +type PeerBehavior interface { + // ReactorChDescriptors returns the Reactor channel descriptors + ReactorChDescriptors() []*conn.ChannelDescriptor + + // Reactors returns the node's active p2p Reactors (modules) + Reactors() map[byte]Reactor + + // HandlePeerError propagates a peer connection error for further processing + HandlePeerError(Peer, error) + + // IsPersistentPeer returns a flag indicating if the given peer is persistent + IsPersistentPeer(types.ID) bool + + // IsPrivatePeer returns a flag indicating if the given peer is private + IsPrivatePeer(types.ID) bool +} + +``` + +In short, the previously-mentioned crux of the `p2p` implementation (having `Peer`s be directly managed by different TM2 +modules) requires information on how to behave when interacting with other peers. + +TM2 modules on `peer A` communicate through something called *channels* to the same modules on `peer B`. For example, if +the `mempool` module on `peer A` wants to share a transaction to the mempool module on `peer B`, it will utilize a +dedicated (and unique!) channel for it (ex. `0x30`). This is a protocol that lives on top of the already-established +multiplexed connection, and metadata relating to it is passed down through *peer behavior*. + +### Transport + +As previously mentioned, the `Transport` is the infrastructure layer of the `p2p` module. + +In contrast to the `Switch`, which is concerned with higher-level application logic (like the number of peers, peer +limits, etc), the `Transport` is concerned with actually establishing and maintaining peer connections on a much lower +level. + +```go +package p2p + +// Transport handles peer dialing and connection acceptance. Additionally, +// it is also responsible for any custom connection mechanisms (like handshaking). +// Peers returned by the transport are considered to be verified and sound +type Transport interface { + // NetAddress returns the Transport's dial address + NetAddress() types.NetAddress + + // Accept returns a newly connected inbound peer + Accept(context.Context, PeerBehavior) (Peer, error) + + // Dial dials a peer, and returns it + Dial(context.Context, types.NetAddress, PeerBehavior) (Peer, error) + + // Remove drops any resources associated + // with the Peer in the transport + Remove(Peer) +} +``` + +When peers dial other peers in TM2, they are in fact dialing their `Transport`s, and the connection is being handled +here. + +- `Accept` waits for an **incoming** connection, parses it and returns it. +- `Dial` attempts to establish an **outgoing** connection, parses it and returns it. + +There are a few important steps that happen when establishing a p2p connection in TM2, between 2 different peers: + +1. The peers go through a handshaking process, and establish something called a *secret connection*. The handshaking + process is based on the [STS protocol](https://github.com/tendermint/tendermint/blob/0.1/docs/sts-final.pdf), and + after it is completed successfully, all communication between the 2 peers is **encrypted**. +2. After establishing a secret connection, the peers exchange their respective node information. The purpose of this + step is to verify that the peers are indeed compatible with each other, and should be establishing a connection in + the first place (same network, common protocols , etc). +3. Once the secret connection is established, and the node information is exchanged, the connection to the peer is + considered valid and verified — it can now be used by the `Switch` (accepted, or rejected, based on `Switch` + high-level constraints). Note the distinction here that the `Transport` establishes and maintains the connection, but + it can ultimately be scraped by the `Switch` at any point in time. + +### Peer Discovery + +There is a final service that runs alongside the previously-mentioned `Switch` services — peer discovery. + +Every blockchain node needs an adequate amount of peers to communicate with, in order to ensure smooth functioning. For +validator nodes, they need to be *loosely connected* to at least 2/3+ of the validator set in order to participate and +not cause block misses or mis-votes (loosely connected means that there always exists a path between different peers in +the network topology, that allows them to be reachable to each other). + +The peer discovery service ensures that the given node is always learning more about the overall network topology, and +filling out any empty connection slots (outbound peers). + +This background service works in the following (albeit primitive) way: + +1. At specific intervals, `node A` checks its peer table, and picks a random peer `P`, from the active peer list. +2. When `P` is picked, `node A` initiates a discovery protocol process, in which: + - `node A` sends a request to peer `P` for his peer list (max 30 peers) + - peer `P` responds to the request + +3. Once `node A` has the peer list from `P`, it adds the entire peer list into the dial queue, to establish outbound + peer connections. + +This process repeats at specific intervals. It is worth nothing that if the limit of outbound peers is reached, the peer +dials have no effect. + +#### Bootnodes (Seeds) + +Bootnodes are specialized network nodes that play a critical role in the initial peer discovery process for new nodes +joining the network. + +When a blockchain client starts, it needs to find and connect to other nodes to synchronize data and participate in the +network. Bootnodes provide a predefined list of accessible and reliable entry points that act as a gateway for +discovering other active nodes (through peer discovery). + +These nodes are provided as part of the node’s p2p configuration. Once connected to a bootnode, the client uses peer +discovery to discover and connect to additional peers, enabling full participation and unlocking other client +protocols (consensus, mempool…). + +Bootnodes usually do not store the full blockchain or participate in consensus; their primary role is to facilitate +connectivity in the network (act as a peer relay). \ No newline at end of file diff --git a/tm2/pkg/p2p/base_reactor.go b/tm2/pkg/p2p/base_reactor.go index 91b3981d109..09d5ff593ab 100644 --- a/tm2/pkg/p2p/base_reactor.go +++ b/tm2/pkg/p2p/base_reactor.go @@ -6,7 +6,7 @@ import ( ) // Reactor is responsible for handling incoming messages on one or more -// Channel. Switch calls GetChannels when reactor is added to it. When a new +// Channel. MultiplexSwitch calls GetChannels when reactor is added to it. When a new // peer joins our node, InitPeer and AddPeer are called. RemovePeer is called // when the peer is stopped. Receive is called when a message is received on a // channel associated with this reactor. @@ -16,7 +16,7 @@ type Reactor interface { service.Service // Start, Stop // SetSwitch allows setting a switch. - SetSwitch(*Switch) + SetSwitch(Switch) // GetChannels returns the list of MConnection.ChannelDescriptor. Make sure // that each ID is unique across all the reactors added to the switch. @@ -47,11 +47,11 @@ type Reactor interface { Receive(chID byte, peer Peer, msgBytes []byte) } -//-------------------------------------- +// -------------------------------------- type BaseReactor struct { - service.BaseService // Provides Start, Stop, .Quit - Switch *Switch + service.BaseService // Provides Start, Stop, Quit + Switch Switch } func NewBaseReactor(name string, impl Reactor) *BaseReactor { @@ -61,11 +61,11 @@ func NewBaseReactor(name string, impl Reactor) *BaseReactor { } } -func (br *BaseReactor) SetSwitch(sw *Switch) { +func (br *BaseReactor) SetSwitch(sw Switch) { br.Switch = sw } -func (*BaseReactor) GetChannels() []*conn.ChannelDescriptor { return nil } -func (*BaseReactor) AddPeer(peer Peer) {} -func (*BaseReactor) RemovePeer(peer Peer, reason interface{}) {} -func (*BaseReactor) Receive(chID byte, peer Peer, msgBytes []byte) {} -func (*BaseReactor) InitPeer(peer Peer) Peer { return peer } +func (*BaseReactor) GetChannels() []*conn.ChannelDescriptor { return nil } +func (*BaseReactor) AddPeer(_ Peer) {} +func (*BaseReactor) RemovePeer(_ Peer, _ any) {} +func (*BaseReactor) Receive(_ byte, _ Peer, _ []byte) {} +func (*BaseReactor) InitPeer(peer Peer) Peer { return peer } diff --git a/tm2/pkg/p2p/cmd/stest/main.go b/tm2/pkg/p2p/cmd/stest/main.go deleted file mode 100644 index 2835e0cc1f0..00000000000 --- a/tm2/pkg/p2p/cmd/stest/main.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "net" - "os" - - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - p2pconn "github.com/gnolang/gno/tm2/pkg/p2p/conn" -) - -var ( - remote string - listen string -) - -func init() { - flag.StringVar(&listen, "listen", "", "set to :port if server, eg :8080") - flag.StringVar(&remote, "remote", "", "remote ip:port") - flag.Parse() -} - -func main() { - if listen != "" { - fmt.Println("listening at", listen) - ln, err := net.Listen("tcp", listen) - if err != nil { - // handle error - } - conn, err := ln.Accept() - if err != nil { - panic(err) - } - handleConnection(conn) - } else { - // connect to remote. - if remote == "" { - panic("must specify remote ip:port unless server") - } - fmt.Println("connecting to", remote) - conn, err := net.Dial("tcp", remote) - if err != nil { - panic(err) - } - handleConnection(conn) - } -} - -func handleConnection(conn net.Conn) { - priv := ed25519.GenPrivKey() - pub := priv.PubKey() - fmt.Println("local pubkey:", pub) - fmt.Println("local pubkey addr:", pub.Address()) - - sconn, err := p2pconn.MakeSecretConnection(conn, priv) - if err != nil { - panic(err) - } - // Read line from sconn and print. - go func() { - sc := bufio.NewScanner(sconn) - for sc.Scan() { - line := sc.Text() // GET the line string - fmt.Println(">>", line) - } - if err := sc.Err(); err != nil { - panic(err) - } - }() - // Read line from stdin and write. - for { - sc := bufio.NewScanner(os.Stdin) - for sc.Scan() { - line := sc.Text() + "\n" - _, err := sconn.Write([]byte(line)) - if err != nil { - panic(err) - } - } - if err := sc.Err(); err != nil { - panic(err) - } - } -} diff --git a/tm2/pkg/p2p/config/config.go b/tm2/pkg/p2p/config/config.go index 07692145fee..6185231af98 100644 --- a/tm2/pkg/p2p/config/config.go +++ b/tm2/pkg/p2p/config/config.go @@ -1,19 +1,15 @@ package config import ( + "errors" "time" - - "github.com/gnolang/gno/tm2/pkg/errors" ) -// ----------------------------------------------------------------------------- -// P2PConfig - -const ( - // FuzzModeDrop is a mode in which we randomly drop reads/writes, connections or sleep - FuzzModeDrop = iota - // FuzzModeDelay is a mode in which we randomly sleep - FuzzModeDelay +var ( + errInvalidFlushThrottleTimeout = errors.New("invalid flush throttle timeout") + errInvalidMaxPayloadSize = errors.New("invalid message payload size") + errInvalidSendRate = errors.New("invalid packet send rate") + errInvalidReceiveRate = errors.New("invalid packet receive rate") ) // P2PConfig defines the configuration options for the Tendermint peer-to-peer networking layer @@ -32,14 +28,11 @@ type P2PConfig struct { // Comma separated list of nodes to keep persistent connections to PersistentPeers string `json:"persistent_peers" toml:"persistent_peers" comment:"Comma separated list of nodes to keep persistent connections to"` - // UPNP port forwarding - UPNP bool `json:"upnp" toml:"upnp" comment:"UPNP port forwarding"` - // Maximum number of inbound peers - MaxNumInboundPeers int `json:"max_num_inbound_peers" toml:"max_num_inbound_peers" comment:"Maximum number of inbound peers"` + MaxNumInboundPeers uint64 `json:"max_num_inbound_peers" toml:"max_num_inbound_peers" comment:"Maximum number of inbound peers"` // Maximum number of outbound peers to connect to, excluding persistent peers - MaxNumOutboundPeers int `json:"max_num_outbound_peers" toml:"max_num_outbound_peers" comment:"Maximum number of outbound peers to connect to, excluding persistent peers"` + MaxNumOutboundPeers uint64 `json:"max_num_outbound_peers" toml:"max_num_outbound_peers" comment:"Maximum number of outbound peers to connect to, excluding persistent peers"` // Time to wait before flushing messages out on the connection FlushThrottleTimeout time.Duration `json:"flush_throttle_timeout" toml:"flush_throttle_timeout" comment:"Time to wait before flushing messages out on the connection"` @@ -54,105 +47,45 @@ type P2PConfig struct { RecvRate int64 `json:"recv_rate" toml:"recv_rate" comment:"Rate at which packets can be received, in bytes/second"` // Set true to enable the peer-exchange reactor - PexReactor bool `json:"pex" toml:"pex" comment:"Set true to enable the peer-exchange reactor"` + PeerExchange bool `json:"pex" toml:"pex" comment:"Set true to enable the peer-exchange reactor"` - // Seed mode, in which node constantly crawls the network and looks for - // peers. If another node asks it for addresses, it responds and disconnects. - // - // Does not work if the peer-exchange reactor is disabled. - SeedMode bool `json:"seed_mode" toml:"seed_mode" comment:"Seed mode, in which node constantly crawls the network and looks for\n peers. If another node asks it for addresses, it responds and disconnects.\n\n Does not work if the peer-exchange reactor is disabled."` - - // Comma separated list of peer IDs to keep private (will not be gossiped to - // other peers) + // Comma separated list of peer IDs to keep private (will not be gossiped to other peers) PrivatePeerIDs string `json:"private_peer_ids" toml:"private_peer_ids" comment:"Comma separated list of peer IDs to keep private (will not be gossiped to other peers)"` - - // Toggle to disable guard against peers connecting from the same ip. - AllowDuplicateIP bool `json:"allow_duplicate_ip" toml:"allow_duplicate_ip" comment:"Toggle to disable guard against peers connecting from the same ip."` - - // Peer connection configuration. - HandshakeTimeout time.Duration `json:"handshake_timeout" toml:"handshake_timeout" comment:"Peer connection configuration."` - DialTimeout time.Duration `json:"dial_timeout" toml:"dial_timeout"` - - // Testing params. - // Force dial to fail - TestDialFail bool `json:"test_dial_fail" toml:"test_dial_fail"` - // FUzz connection - TestFuzz bool `json:"test_fuzz" toml:"test_fuzz"` - TestFuzzConfig *FuzzConnConfig `json:"test_fuzz_config" toml:"test_fuzz_config"` } // DefaultP2PConfig returns a default configuration for the peer-to-peer layer func DefaultP2PConfig() *P2PConfig { return &P2PConfig{ ListenAddress: "tcp://0.0.0.0:26656", - ExternalAddress: "", - UPNP: false, + ExternalAddress: "", // nothing is advertised differently MaxNumInboundPeers: 40, MaxNumOutboundPeers: 10, FlushThrottleTimeout: 100 * time.Millisecond, MaxPacketMsgPayloadSize: 1024, // 1 kB SendRate: 5120000, // 5 mB/s RecvRate: 5120000, // 5 mB/s - PexReactor: true, - SeedMode: false, - AllowDuplicateIP: false, - HandshakeTimeout: 20 * time.Second, - DialTimeout: 3 * time.Second, - TestDialFail: false, - TestFuzz: false, - TestFuzzConfig: DefaultFuzzConnConfig(), + PeerExchange: true, } } -// TestP2PConfig returns a configuration for testing the peer-to-peer layer -func TestP2PConfig() *P2PConfig { - cfg := DefaultP2PConfig() - cfg.ListenAddress = "tcp://0.0.0.0:26656" - cfg.FlushThrottleTimeout = 10 * time.Millisecond - cfg.AllowDuplicateIP = true - return cfg -} - // ValidateBasic performs basic validation (checking param bounds, etc.) and // returns an error if any check fails. func (cfg *P2PConfig) ValidateBasic() error { - if cfg.MaxNumInboundPeers < 0 { - return errors.New("max_num_inbound_peers can't be negative") - } - if cfg.MaxNumOutboundPeers < 0 { - return errors.New("max_num_outbound_peers can't be negative") - } if cfg.FlushThrottleTimeout < 0 { - return errors.New("flush_throttle_timeout can't be negative") + return errInvalidFlushThrottleTimeout } + if cfg.MaxPacketMsgPayloadSize < 0 { - return errors.New("max_packet_msg_payload_size can't be negative") + return errInvalidMaxPayloadSize } + if cfg.SendRate < 0 { - return errors.New("send_rate can't be negative") + return errInvalidSendRate } + if cfg.RecvRate < 0 { - return errors.New("recv_rate can't be negative") + return errInvalidReceiveRate } - return nil -} - -// FuzzConnConfig is a FuzzedConnection configuration. -type FuzzConnConfig struct { - Mode int - MaxDelay time.Duration - ProbDropRW float64 - ProbDropConn float64 - ProbSleep float64 -} -// DefaultFuzzConnConfig returns the default config. -func DefaultFuzzConnConfig() *FuzzConnConfig { - return &FuzzConnConfig{ - Mode: FuzzModeDrop, - MaxDelay: 3 * time.Second, - ProbDropRW: 0.2, - ProbDropConn: 0.00, - ProbSleep: 0.00, - } + return nil } diff --git a/tm2/pkg/p2p/config/config_test.go b/tm2/pkg/p2p/config/config_test.go new file mode 100644 index 00000000000..6528c2d7315 --- /dev/null +++ b/tm2/pkg/p2p/config/config_test.go @@ -0,0 +1,59 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestP2PConfig_ValidateBasic(t *testing.T) { + t.Parallel() + + t.Run("invalid flush throttle timeout", func(t *testing.T) { + t.Parallel() + + cfg := DefaultP2PConfig() + + cfg.FlushThrottleTimeout = -1 + + assert.ErrorIs(t, cfg.ValidateBasic(), errInvalidFlushThrottleTimeout) + }) + + t.Run("invalid max packet payload size", func(t *testing.T) { + t.Parallel() + + cfg := DefaultP2PConfig() + + cfg.MaxPacketMsgPayloadSize = -1 + + assert.ErrorIs(t, cfg.ValidateBasic(), errInvalidMaxPayloadSize) + }) + + t.Run("invalid send rate", func(t *testing.T) { + t.Parallel() + + cfg := DefaultP2PConfig() + + cfg.SendRate = -1 + + assert.ErrorIs(t, cfg.ValidateBasic(), errInvalidSendRate) + }) + + t.Run("invalid receive rate", func(t *testing.T) { + t.Parallel() + + cfg := DefaultP2PConfig() + + cfg.RecvRate = -1 + + assert.ErrorIs(t, cfg.ValidateBasic(), errInvalidReceiveRate) + }) + + t.Run("valid configuration", func(t *testing.T) { + t.Parallel() + + cfg := DefaultP2PConfig() + + assert.NoError(t, cfg.ValidateBasic()) + }) +} diff --git a/tm2/pkg/p2p/conn/conn.go b/tm2/pkg/p2p/conn/conn.go new file mode 100644 index 00000000000..3215adc38ca --- /dev/null +++ b/tm2/pkg/p2p/conn/conn.go @@ -0,0 +1,22 @@ +package conn + +import ( + "net" + "time" +) + +// pipe wraps the networking conn interface +type pipe struct { + net.Conn +} + +func (p *pipe) SetDeadline(_ time.Time) error { + return nil +} + +func NetPipe() (net.Conn, net.Conn) { + p1, p2 := net.Pipe() + return &pipe{p1}, &pipe{p2} +} + +var _ net.Conn = (*pipe)(nil) diff --git a/tm2/pkg/p2p/conn/conn_go110.go b/tm2/pkg/p2p/conn/conn_go110.go deleted file mode 100644 index 37796ac791d..00000000000 --- a/tm2/pkg/p2p/conn/conn_go110.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build go1.10 - -package conn - -// Go1.10 has a proper net.Conn implementation that -// has the SetDeadline method implemented as per -// https://github.com/golang/go/commit/e2dd8ca946be884bb877e074a21727f1a685a706 -// lest we run into problems like -// https://github.com/tendermint/classic/issues/851 - -import "net" - -func NetPipe() (net.Conn, net.Conn) { - return net.Pipe() -} diff --git a/tm2/pkg/p2p/conn/conn_notgo110.go b/tm2/pkg/p2p/conn/conn_notgo110.go deleted file mode 100644 index f91b0c7ea63..00000000000 --- a/tm2/pkg/p2p/conn/conn_notgo110.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build !go1.10 - -package conn - -import ( - "net" - "time" -) - -// Only Go1.10 has a proper net.Conn implementation that -// has the SetDeadline method implemented as per -// -// https://github.com/golang/go/commit/e2dd8ca946be884bb877e074a21727f1a685a706 -// -// lest we run into problems like -// -// https://github.com/tendermint/classic/issues/851 -// -// so for go versions < Go1.10 use our custom net.Conn creator -// that doesn't return an `Unimplemented error` for net.Conn. -// Before https://github.com/tendermint/classic/commit/49faa79bdce5663894b3febbf4955fb1d172df04 -// we hadn't cared about errors from SetDeadline so swallow them up anyways. -type pipe struct { - net.Conn -} - -func (p *pipe) SetDeadline(t time.Time) error { - return nil -} - -func NetPipe() (net.Conn, net.Conn) { - p1, p2 := net.Pipe() - return &pipe{p1}, &pipe{p2} -} - -var _ net.Conn = (*pipe)(nil) diff --git a/tm2/pkg/p2p/conn/connection.go b/tm2/pkg/p2p/conn/connection.go index 6b7400600d3..acbffdb3cd7 100644 --- a/tm2/pkg/p2p/conn/connection.go +++ b/tm2/pkg/p2p/conn/connection.go @@ -17,6 +17,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/flow" + "github.com/gnolang/gno/tm2/pkg/p2p/config" "github.com/gnolang/gno/tm2/pkg/service" "github.com/gnolang/gno/tm2/pkg/timer" ) @@ -31,7 +32,6 @@ const ( // some of these defaults are written in the user config // flushThrottle, sendRate, recvRate - // TODO: remove values present in config defaultFlushThrottle = 100 * time.Millisecond defaultSendQueueCapacity = 1 @@ -46,7 +46,7 @@ const ( type ( receiveCbFunc func(chID byte, msgBytes []byte) - errorCbFunc func(interface{}) + errorCbFunc func(error) ) /* @@ -147,6 +147,18 @@ func DefaultMConnConfig() MConnConfig { } } +// MConfigFromP2P returns a multiplex connection configuration +// with fields updated from the P2PConfig +func MConfigFromP2P(cfg *config.P2PConfig) MConnConfig { + mConfig := DefaultMConnConfig() + mConfig.FlushThrottle = cfg.FlushThrottleTimeout + mConfig.SendRate = cfg.SendRate + mConfig.RecvRate = cfg.RecvRate + mConfig.MaxPacketMsgPayloadSize = cfg.MaxPacketMsgPayloadSize + + return mConfig +} + // NewMConnection wraps net.Conn and creates multiplex connection func NewMConnection(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc) *MConnection { return NewMConnectionWithConfig( @@ -323,7 +335,7 @@ func (c *MConnection) _recover() { } } -func (c *MConnection) stopForError(r interface{}) { +func (c *MConnection) stopForError(r error) { c.Stop() if atomic.CompareAndSwapUint32(&c.errored, 0, 1) { if c.onError != nil { diff --git a/tm2/pkg/p2p/conn/connection_test.go b/tm2/pkg/p2p/conn/connection_test.go index 7bbe88ded22..58b363b7b78 100644 --- a/tm2/pkg/p2p/conn/connection_test.go +++ b/tm2/pkg/p2p/conn/connection_test.go @@ -21,7 +21,7 @@ func createTestMConnection(t *testing.T, conn net.Conn) *MConnection { onReceive := func(chID byte, msgBytes []byte) { } - onError := func(r interface{}) { + onError := func(r error) { } c := createMConnectionWithCallbacks(t, conn, onReceive, onError) c.SetLogger(log.NewTestingLogger(t)) @@ -32,7 +32,7 @@ func createMConnectionWithCallbacks( t *testing.T, conn net.Conn, onReceive func(chID byte, msgBytes []byte), - onError func(r interface{}), + onError func(r error), ) *MConnection { t.Helper() @@ -137,7 +137,7 @@ func TestMConnectionReceive(t *testing.T) { onReceive := func(chID byte, msgBytes []byte) { receivedCh <- msgBytes } - onError := func(r interface{}) { + onError := func(r error) { errorsCh <- r } mconn1 := createMConnectionWithCallbacks(t, client, onReceive, onError) @@ -192,7 +192,7 @@ func TestMConnectionPongTimeoutResultsInError(t *testing.T) { onReceive := func(chID byte, msgBytes []byte) { receivedCh <- msgBytes } - onError := func(r interface{}) { + onError := func(r error) { errorsCh <- r } mconn := createMConnectionWithCallbacks(t, client, onReceive, onError) @@ -233,7 +233,7 @@ func TestMConnectionMultiplePongsInTheBeginning(t *testing.T) { onReceive := func(chID byte, msgBytes []byte) { receivedCh <- msgBytes } - onError := func(r interface{}) { + onError := func(r error) { errorsCh <- r } mconn := createMConnectionWithCallbacks(t, client, onReceive, onError) @@ -288,7 +288,7 @@ func TestMConnectionMultiplePings(t *testing.T) { onReceive := func(chID byte, msgBytes []byte) { receivedCh <- msgBytes } - onError := func(r interface{}) { + onError := func(r error) { errorsCh <- r } mconn := createMConnectionWithCallbacks(t, client, onReceive, onError) @@ -331,7 +331,7 @@ func TestMConnectionPingPongs(t *testing.T) { onReceive := func(chID byte, msgBytes []byte) { receivedCh <- msgBytes } - onError := func(r interface{}) { + onError := func(r error) { errorsCh <- r } mconn := createMConnectionWithCallbacks(t, client, onReceive, onError) @@ -384,7 +384,7 @@ func TestMConnectionStopsAndReturnsError(t *testing.T) { onReceive := func(chID byte, msgBytes []byte) { receivedCh <- msgBytes } - onError := func(r interface{}) { + onError := func(r error) { errorsCh <- r } mconn := createMConnectionWithCallbacks(t, client, onReceive, onError) @@ -413,7 +413,7 @@ func newClientAndServerConnsForReadErrors(t *testing.T, chOnErr chan struct{}) ( server, client := NetPipe() onReceive := func(chID byte, msgBytes []byte) {} - onError := func(r interface{}) {} + onError := func(r error) {} // create client conn with two channels chDescs := []*ChannelDescriptor{ @@ -428,7 +428,7 @@ func newClientAndServerConnsForReadErrors(t *testing.T, chOnErr chan struct{}) ( // create server conn with 1 channel // it fires on chOnErr when there's an error serverLogger := log.NewNoopLogger().With("module", "server") - onError = func(r interface{}) { + onError = func(_ error) { chOnErr <- struct{}{} } mconnServer := createMConnectionWithCallbacks(t, server, onReceive, onError) diff --git a/tm2/pkg/p2p/conn/secret_connection.go b/tm2/pkg/p2p/conn/secret_connection.go index a37788b947d..d45b5b3846a 100644 --- a/tm2/pkg/p2p/conn/secret_connection.go +++ b/tm2/pkg/p2p/conn/secret_connection.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/binary" + "fmt" "io" "math" "net" @@ -128,7 +129,10 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (* } // Sign the challenge bytes for authentication. - locSignature := signChallenge(challenge, locPrivKey) + locSignature, err := locPrivKey.Sign(challenge[:]) + if err != nil { + return nil, fmt.Errorf("unable to sign challenge, %w", err) + } // Share (in secret) each other's pubkey & challenge signature authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature) @@ -424,15 +428,6 @@ func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) { return } -func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature []byte) { - signature, err := locPrivKey.Sign(challenge[:]) - // TODO(ismail): let signChallenge return an error instead - if err != nil { - panic(err) - } - return -} - type authSigMessage struct { Key crypto.PubKey Sig []byte diff --git a/tm2/pkg/p2p/conn_set.go b/tm2/pkg/p2p/conn_set.go deleted file mode 100644 index d646227831a..00000000000 --- a/tm2/pkg/p2p/conn_set.go +++ /dev/null @@ -1,81 +0,0 @@ -package p2p - -import ( - "net" - "sync" -) - -// ConnSet is a lookup table for connections and all their ips. -type ConnSet interface { - Has(net.Conn) bool - HasIP(net.IP) bool - Set(net.Conn, []net.IP) - Remove(net.Conn) - RemoveAddr(net.Addr) -} - -type connSetItem struct { - conn net.Conn - ips []net.IP -} - -type connSet struct { - sync.RWMutex - - conns map[string]connSetItem -} - -// NewConnSet returns a ConnSet implementation. -func NewConnSet() *connSet { - return &connSet{ - conns: map[string]connSetItem{}, - } -} - -func (cs *connSet) Has(c net.Conn) bool { - cs.RLock() - defer cs.RUnlock() - - _, ok := cs.conns[c.RemoteAddr().String()] - - return ok -} - -func (cs *connSet) HasIP(ip net.IP) bool { - cs.RLock() - defer cs.RUnlock() - - for _, c := range cs.conns { - for _, known := range c.ips { - if known.Equal(ip) { - return true - } - } - } - - return false -} - -func (cs *connSet) Remove(c net.Conn) { - cs.Lock() - defer cs.Unlock() - - delete(cs.conns, c.RemoteAddr().String()) -} - -func (cs *connSet) RemoveAddr(addr net.Addr) { - cs.Lock() - defer cs.Unlock() - - delete(cs.conns, addr.String()) -} - -func (cs *connSet) Set(c net.Conn, ips []net.IP) { - cs.Lock() - defer cs.Unlock() - - cs.conns[c.RemoteAddr().String()] = connSetItem{ - conn: c, - ips: ips, - } -} diff --git a/tm2/pkg/p2p/dial/dial.go b/tm2/pkg/p2p/dial/dial.go new file mode 100644 index 00000000000..e4a7d6fd445 --- /dev/null +++ b/tm2/pkg/p2p/dial/dial.go @@ -0,0 +1,83 @@ +package dial + +import ( + "sync" + "time" + + "github.com/gnolang/gno/tm2/pkg/p2p/types" + queue "github.com/sig-0/insertion-queue" +) + +// Item is a single dial queue item, wrapping +// the approximately appropriate dial time, and the +// peer dial address +type Item struct { + Time time.Time // appropriate dial time + Address *types.NetAddress // the dial address of the peer +} + +// Less is the comparison method for the dial queue Item (time ascending) +func (i Item) Less(item Item) bool { + return i.Time.Before(item.Time) +} + +// Queue is a time-sorted (ascending) dial queue +type Queue struct { + mux sync.RWMutex + + items queue.Queue[Item] // sorted dial queue (by time, ascending) +} + +// NewQueue creates a new dial queue +func NewQueue() *Queue { + return &Queue{ + items: queue.NewQueue[Item](), + } +} + +// Peek returns the first item in the dial queue, if any +func (q *Queue) Peek() *Item { + q.mux.RLock() + defer q.mux.RUnlock() + + if q.items.Len() == 0 { + return nil + } + + item := q.items.Index(0) + + return &item +} + +// Push adds new items to the dial queue +func (q *Queue) Push(items ...Item) { + q.mux.Lock() + defer q.mux.Unlock() + + for _, item := range items { + q.items.Push(item) + } +} + +// Pop removes an item from the dial queue, if any +func (q *Queue) Pop() *Item { + q.mux.Lock() + defer q.mux.Unlock() + + return q.items.PopFront() +} + +// Has returns a flag indicating if the given +// address is in the dial queue +func (q *Queue) Has(addr *types.NetAddress) bool { + q.mux.RLock() + defer q.mux.RUnlock() + + for _, i := range q.items { + if addr.Equals(*i.Address) { + return true + } + } + + return false +} diff --git a/tm2/pkg/p2p/dial/dial_test.go b/tm2/pkg/p2p/dial/dial_test.go new file mode 100644 index 00000000000..5e85ec1f95e --- /dev/null +++ b/tm2/pkg/p2p/dial/dial_test.go @@ -0,0 +1,147 @@ +package dial + +import ( + "crypto/rand" + "math/big" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateRandomTimes generates random time intervals +func generateRandomTimes(t *testing.T, count int) []time.Time { + t.Helper() + + const timeRange = 94608000 // 3 years + + var ( + maxRange = big.NewInt(time.Now().Unix() - timeRange) + times = make([]time.Time, 0, count) + ) + + for range count { + n, err := rand.Int(rand.Reader, maxRange) + require.NoError(t, err) + + randTime := time.Unix(n.Int64()+timeRange, 0) + + times = append(times, randTime) + } + + return times +} + +func TestQueue_Push(t *testing.T) { + t.Parallel() + + var ( + timestamps = generateRandomTimes(t, 10) + q = NewQueue() + ) + + // Add the dial items + for _, timestamp := range timestamps { + q.Push(Item{ + Time: timestamp, + }) + } + + assert.Len(t, q.items, len(timestamps)) +} + +func TestQueue_Peek(t *testing.T) { + t.Parallel() + + t.Run("empty queue", func(t *testing.T) { + t.Parallel() + + q := NewQueue() + + assert.Nil(t, q.Peek()) + }) + + t.Run("existing item", func(t *testing.T) { + t.Parallel() + + var ( + timestamps = generateRandomTimes(t, 100) + q = NewQueue() + ) + + // Add the dial items + for _, timestamp := range timestamps { + q.Push(Item{ + Time: timestamp, + }) + } + + // Sort the initial list to find the best timestamp + slices.SortFunc(timestamps, func(a, b time.Time) int { + if a.Before(b) { + return -1 + } + + if a.After(b) { + return 1 + } + + return 0 + }) + + assert.Equal(t, q.Peek().Time.Unix(), timestamps[0].Unix()) + }) +} + +func TestQueue_Pop(t *testing.T) { + t.Parallel() + + t.Run("empty queue", func(t *testing.T) { + t.Parallel() + + q := NewQueue() + + assert.Nil(t, q.Pop()) + }) + + t.Run("existing item", func(t *testing.T) { + t.Parallel() + + var ( + timestamps = generateRandomTimes(t, 100) + q = NewQueue() + ) + + // Add the dial items + for _, timestamp := range timestamps { + q.Push(Item{ + Time: timestamp, + }) + } + + assert.Len(t, q.items, len(timestamps)) + + // Sort the initial list to find the best timestamp + slices.SortFunc(timestamps, func(a, b time.Time) int { + if a.Before(b) { + return -1 + } + + if a.After(b) { + return 1 + } + + return 0 + }) + + for index, timestamp := range timestamps { + item := q.Pop() + + require.Len(t, q.items, len(timestamps)-1-index) + + assert.Equal(t, item.Time.Unix(), timestamp.Unix()) + } + }) +} diff --git a/tm2/pkg/p2p/dial/doc.go b/tm2/pkg/p2p/dial/doc.go new file mode 100644 index 00000000000..069160e73e6 --- /dev/null +++ b/tm2/pkg/p2p/dial/doc.go @@ -0,0 +1,10 @@ +// Package dial contains an implementation of a thread-safe priority dial queue. The queue is sorted by +// dial items, time ascending. +// The behavior of the dial queue is the following: +// +// - Peeking the dial queue will return the most urgent dial item, or nil if the queue is empty. +// +// - Popping the dial queue will return the most urgent dial item or nil if the queue is empty. Popping removes the dial item. +// +// - Push will push a new item to the dial queue, upon which the queue will find an adequate place for it. +package dial diff --git a/tm2/pkg/p2p/discovery/discovery.go b/tm2/pkg/p2p/discovery/discovery.go new file mode 100644 index 00000000000..e07ef08b323 --- /dev/null +++ b/tm2/pkg/p2p/discovery/discovery.go @@ -0,0 +1,243 @@ +package discovery + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + "time" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "golang.org/x/exp/slices" +) + +const ( + // Channel is the unique channel for the peer discovery protocol + Channel = byte(0x50) + + // discoveryInterval is the peer discovery interval, for random peers + discoveryInterval = time.Second * 3 + + // maxPeersShared is the maximum number of peers shared in the discovery request + maxPeersShared = 30 +) + +// descriptor is the constant peer discovery protocol descriptor +var descriptor = &conn.ChannelDescriptor{ + ID: Channel, + Priority: 1, // peer discovery is high priority + SendQueueCapacity: 20, // more than enough active conns + RecvMessageCapacity: 5242880, // 5MB +} + +// Reactor wraps the logic for the peer exchange protocol +type Reactor struct { + // This embed and the usage of "services" + // like the peer discovery reactor highlight the + // flipped design of the p2p package. + // The peer exchange service needs to be instantiated _outside_ + // the p2p module, because of this flipped design. + // Peers communicate with each other through Reactor channels, + // which are instantiated outside the p2p module + p2p.BaseReactor + + ctx context.Context + cancelFn context.CancelFunc + + discoveryInterval time.Duration +} + +// NewReactor creates a new peer discovery reactor +func NewReactor(opts ...Option) *Reactor { + ctx, cancelFn := context.WithCancel(context.Background()) + + r := &Reactor{ + ctx: ctx, + cancelFn: cancelFn, + discoveryInterval: discoveryInterval, + } + + r.BaseReactor = *p2p.NewBaseReactor("Reactor", r) + + // Apply the options + for _, opt := range opts { + opt(r) + } + + return r +} + +// OnStart runs the peer discovery protocol +func (r *Reactor) OnStart() error { + go func() { + ticker := time.NewTicker(r.discoveryInterval) + defer ticker.Stop() + + for { + select { + case <-r.ctx.Done(): + r.Logger.Debug("discovery service stopped") + + return + case <-ticker.C: + // Run the discovery protocol // + + // Grab a random peer, and engage + // them for peer discovery + peers := r.Switch.Peers().List() + + if len(peers) == 0 { + // No discovery to run + continue + } + + // Generate a random peer index + randomPeer, _ := rand.Int( + rand.Reader, + big.NewInt(int64(len(peers))), + ) + + // Request peers, async + go r.requestPeers(peers[randomPeer.Int64()]) + } + } + }() + + return nil +} + +// OnStop stops the peer discovery protocol +func (r *Reactor) OnStop() { + r.cancelFn() +} + +// requestPeers requests the peer set from the given peer +func (r *Reactor) requestPeers(peer p2p.Peer) { + // Initiate peer discovery + r.Logger.Debug("running peer discovery", "peer", peer.ID()) + + // Prepare the request + // (empty, as it's a notification) + req := &Request{} + + reqBytes, err := amino.MarshalAny(req) + if err != nil { + r.Logger.Error("unable to marshal discovery request", "err", err) + + return + } + + // Send the request + if !peer.Send(Channel, reqBytes) { + r.Logger.Warn("unable to send discovery request", "peer", peer.ID()) + } +} + +// GetChannels returns the channels associated with peer discovery +func (r *Reactor) GetChannels() []*conn.ChannelDescriptor { + return []*conn.ChannelDescriptor{descriptor} +} + +// Receive handles incoming messages for the peer discovery reactor +func (r *Reactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) { + r.Logger.Debug( + "received message", + "peerID", peer.ID(), + "chID", chID, + ) + + // Unmarshal the message + var msg Message + + if err := amino.UnmarshalAny(msgBytes, &msg); err != nil { + r.Logger.Error("unable to unmarshal discovery message", "err", err) + + return + } + + // Validate the message + if err := msg.ValidateBasic(); err != nil { + r.Logger.Error("unable to validate discovery message", "err", err) + + return + } + + switch msg := msg.(type) { + case *Request: + if err := r.handleDiscoveryRequest(peer); err != nil { + r.Logger.Error("unable to handle discovery request", "err", err) + } + case *Response: + // Make the peers available for dialing on the switch + r.Switch.DialPeers(msg.Peers...) + default: + r.Logger.Warn("invalid message received", "msg", msgBytes) + } +} + +// handleDiscoveryRequest prepares a peer list that can be shared +// with the peer requesting discovery +func (r *Reactor) handleDiscoveryRequest(peer p2p.Peer) error { + // Check if there is anything to share, + // to avoid useless traffic + switchPeers := r.Switch.Peers() + if switchPeers.NumOutbound()+switchPeers.NumInbound() == 0 { + r.Logger.Warn("no peers to share in discovery request") + + return nil + } + + var ( + localPeers = r.Switch.Peers().List() + peers = make([]*types.NetAddress, 0, len(localPeers)) + ) + + // Exclude the private peers from being shared + localPeers = slices.DeleteFunc(localPeers, func(p p2p.Peer) bool { + return p.IsPrivate() + }) + + // Shuffle and limit the peers shared + shufflePeers(localPeers) + + if len(localPeers) > maxPeersShared { + localPeers = localPeers[:maxPeersShared] + } + + for _, p := range localPeers { + peers = append(peers, p.SocketAddr()) + } + + // Create the response, and marshal + // it to Amino binary + resp := &Response{ + Peers: peers, + } + + preparedResp, err := amino.MarshalAny(resp) + if err != nil { + return fmt.Errorf("unable to marshal discovery response, %w", err) + } + + // Send the response to the peer + if !peer.Send(Channel, preparedResp) { + return fmt.Errorf("unable to send discovery response to peer %s", peer.ID()) + } + + return nil +} + +// shufflePeers shuffles the peer list in-place +func shufflePeers(peers []p2p.Peer) { + for i := len(peers) - 1; i > 0; i-- { + jBig, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1))) + + j := int(jBig.Int64()) + + // Swap elements + peers[i], peers[j] = peers[j], peers[i] + } +} diff --git a/tm2/pkg/p2p/discovery/discovery_test.go b/tm2/pkg/p2p/discovery/discovery_test.go new file mode 100644 index 00000000000..4d1a1309d52 --- /dev/null +++ b/tm2/pkg/p2p/discovery/discovery_test.go @@ -0,0 +1,453 @@ +package discovery + +import ( + "slices" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/mock" + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReactor_DiscoveryRequest(t *testing.T) { + t.Parallel() + + var ( + notifCh = make(chan struct{}, 1) + + capturedSend []byte + + mockPeer = &mock.Peer{ + SendFn: func(chID byte, data []byte) bool { + require.Equal(t, Channel, chID) + + capturedSend = data + + notifCh <- struct{}{} + + return true + }, + } + + ps = &mockPeerSet{ + listFn: func() []p2p.Peer { + return []p2p.Peer{mockPeer} + }, + } + + mockSwitch = &mockSwitch{ + peersFn: func() p2p.PeerSet { + return ps + }, + } + ) + + r := NewReactor( + WithDiscoveryInterval(10 * time.Millisecond), + ) + + // Set the mock switch + r.SetSwitch(mockSwitch) + + // Start the discovery service + require.NoError(t, r.Start()) + t.Cleanup(func() { + require.NoError(t, r.Stop()) + }) + + select { + case <-notifCh: + case <-time.After(5 * time.Second): + } + + // Make sure the adequate message was captured + require.NotNil(t, capturedSend) + + // Parse the message + var msg Message + + require.NoError(t, amino.Unmarshal(capturedSend, &msg)) + + // Make sure the base message is valid + require.NoError(t, msg.ValidateBasic()) + + _, ok := msg.(*Request) + + require.True(t, ok) +} + +func TestReactor_DiscoveryResponse(t *testing.T) { + t.Parallel() + + t.Run("discovery request received", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 50) + notifCh = make(chan struct{}, 1) + + capturedSend []byte + + mockPeer = &mock.Peer{ + SendFn: func(chID byte, data []byte) bool { + require.Equal(t, Channel, chID) + + capturedSend = data + + notifCh <- struct{}{} + + return true + }, + } + + ps = &mockPeerSet{ + listFn: func() []p2p.Peer { + listed := make([]p2p.Peer, 0, len(peers)) + + for _, peer := range peers { + listed = append(listed, peer) + } + + return listed + }, + numInboundFn: func() uint64 { + return uint64(len(peers)) + }, + } + + mockSwitch = &mockSwitch{ + peersFn: func() p2p.PeerSet { + return ps + }, + } + ) + + r := NewReactor( + WithDiscoveryInterval(10 * time.Millisecond), + ) + + // Set the mock switch + r.SetSwitch(mockSwitch) + + // Prepare the message + req := &Request{} + + preparedReq, err := amino.MarshalAny(req) + require.NoError(t, err) + + // Receive the message + r.Receive(Channel, mockPeer, preparedReq) + + select { + case <-notifCh: + case <-time.After(5 * time.Second): + } + + // Make sure the adequate message was captured + require.NotNil(t, capturedSend) + + // Parse the message + var msg Message + + require.NoError(t, amino.Unmarshal(capturedSend, &msg)) + + // Make sure the base message is valid + require.NoError(t, msg.ValidateBasic()) + + resp, ok := msg.(*Response) + require.True(t, ok) + + // Make sure the peers are valid + require.Len(t, resp.Peers, maxPeersShared) + + slices.ContainsFunc(resp.Peers, func(addr *types.NetAddress) bool { + for _, localP := range peers { + if localP.SocketAddr().Equals(*addr) { + return true + } + } + + return false + }) + }) + + t.Run("empty peers on discover", func(t *testing.T) { + t.Parallel() + + var ( + capturedSend []byte + + mockPeer = &mock.Peer{ + SendFn: func(chID byte, data []byte) bool { + require.Equal(t, Channel, chID) + + capturedSend = data + + return true + }, + } + + ps = &mockPeerSet{ + listFn: func() []p2p.Peer { + return make([]p2p.Peer, 0) + }, + } + + mockSwitch = &mockSwitch{ + peersFn: func() p2p.PeerSet { + return ps + }, + } + ) + + r := NewReactor( + WithDiscoveryInterval(10 * time.Millisecond), + ) + + // Set the mock switch + r.SetSwitch(mockSwitch) + + // Prepare the message + req := &Request{} + + preparedReq, err := amino.MarshalAny(req) + require.NoError(t, err) + + // Receive the message + r.Receive(Channel, mockPeer, preparedReq) + + // Make sure no message was captured + assert.Nil(t, capturedSend) + }) + + t.Run("private peers not shared", func(t *testing.T) { + t.Parallel() + + var ( + publicPeers = 1 + privatePeers = 50 + + peers = mock.GeneratePeers(t, publicPeers+privatePeers) + notifCh = make(chan struct{}, 1) + + capturedSend []byte + + mockPeer = &mock.Peer{ + SendFn: func(chID byte, data []byte) bool { + require.Equal(t, Channel, chID) + + capturedSend = data + + notifCh <- struct{}{} + + return true + }, + } + + ps = &mockPeerSet{ + listFn: func() []p2p.Peer { + listed := make([]p2p.Peer, 0, len(peers)) + + for _, peer := range peers { + listed = append(listed, peer) + } + + return listed + }, + numInboundFn: func() uint64 { + return uint64(len(peers)) + }, + } + + mockSwitch = &mockSwitch{ + peersFn: func() p2p.PeerSet { + return ps + }, + } + ) + + // Mark all except the last X peers as private + for _, peer := range peers[:privatePeers] { + peer.IsPrivateFn = func() bool { + return true + } + } + + r := NewReactor( + WithDiscoveryInterval(10 * time.Millisecond), + ) + + // Set the mock switch + r.SetSwitch(mockSwitch) + + // Prepare the message + req := &Request{} + + preparedReq, err := amino.MarshalAny(req) + require.NoError(t, err) + + // Receive the message + r.Receive(Channel, mockPeer, preparedReq) + + select { + case <-notifCh: + case <-time.After(5 * time.Second): + } + + // Make sure the adequate message was captured + require.NotNil(t, capturedSend) + + // Parse the message + var msg Message + + require.NoError(t, amino.Unmarshal(capturedSend, &msg)) + + // Make sure the base message is valid + require.NoError(t, msg.ValidateBasic()) + + resp, ok := msg.(*Response) + require.True(t, ok) + + // Make sure the peers are valid + require.Len(t, resp.Peers, publicPeers) + + slices.ContainsFunc(resp.Peers, func(addr *types.NetAddress) bool { + for _, localP := range peers { + if localP.SocketAddr().Equals(*addr) { + return true + } + } + + return false + }) + }) + + t.Run("peer response received", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 50) + notifCh = make(chan struct{}, 1) + + capturedDials []*types.NetAddress + + ps = &mockPeerSet{ + listFn: func() []p2p.Peer { + listed := make([]p2p.Peer, 0, len(peers)) + + for _, peer := range peers { + listed = append(listed, peer) + } + + return listed + }, + numInboundFn: func() uint64 { + return uint64(len(peers)) + }, + } + + mockSwitch = &mockSwitch{ + peersFn: func() p2p.PeerSet { + return ps + }, + dialPeersFn: func(addresses ...*types.NetAddress) { + capturedDials = append(capturedDials, addresses...) + + notifCh <- struct{}{} + }, + } + ) + + r := NewReactor( + WithDiscoveryInterval(10 * time.Millisecond), + ) + + // Set the mock switch + r.SetSwitch(mockSwitch) + + // Prepare the addresses + peerAddrs := make([]*types.NetAddress, 0, len(peers)) + + for _, p := range peers { + peerAddrs = append(peerAddrs, p.SocketAddr()) + } + + // Prepare the message + req := &Response{ + Peers: peerAddrs, + } + + preparedReq, err := amino.MarshalAny(req) + require.NoError(t, err) + + // Receive the message + r.Receive(Channel, &mock.Peer{}, preparedReq) + + select { + case <-notifCh: + case <-time.After(5 * time.Second): + } + + // Make sure the correct peers were dialed + assert.Equal(t, capturedDials, peerAddrs) + }) + + t.Run("invalid peer response received", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 50) + + capturedDials []*types.NetAddress + + ps = &mockPeerSet{ + listFn: func() []p2p.Peer { + listed := make([]p2p.Peer, 0, len(peers)) + + for _, peer := range peers { + listed = append(listed, peer) + } + + return listed + }, + numInboundFn: func() uint64 { + return uint64(len(peers)) + }, + } + + mockSwitch = &mockSwitch{ + peersFn: func() p2p.PeerSet { + return ps + }, + dialPeersFn: func(addresses ...*types.NetAddress) { + capturedDials = append(capturedDials, addresses...) + }, + } + ) + + r := NewReactor( + WithDiscoveryInterval(10 * time.Millisecond), + ) + + // Set the mock switch + r.SetSwitch(mockSwitch) + + // Prepare the message + req := &Response{ + Peers: make([]*types.NetAddress, 0), // empty + } + + preparedReq, err := amino.MarshalAny(req) + require.NoError(t, err) + + // Receive the message + r.Receive(Channel, &mock.Peer{}, preparedReq) + + // Make sure no peers were dialed + assert.Empty(t, capturedDials) + }) +} diff --git a/tm2/pkg/p2p/discovery/doc.go b/tm2/pkg/p2p/discovery/doc.go new file mode 100644 index 00000000000..5426bb41277 --- /dev/null +++ b/tm2/pkg/p2p/discovery/doc.go @@ -0,0 +1,9 @@ +// Package discovery contains the p2p peer discovery service (Reactor). +// The purpose of the peer discovery service is to gather peer lists from known peers, +// and attempt to fill out open peer connection slots in order to build out a fuller mesh. +// +// The implementation of the peer discovery protocol is relatively simple. +// In essence, it pings a random peer at a specific interval (3s), for a list of their known peers (max 30). +// After receiving the list, and verifying it, the node attempts to establish outbound connections to the +// given peers. +package discovery diff --git a/tm2/pkg/p2p/discovery/mock_test.go b/tm2/pkg/p2p/discovery/mock_test.go new file mode 100644 index 00000000000..03919a673d3 --- /dev/null +++ b/tm2/pkg/p2p/discovery/mock_test.go @@ -0,0 +1,135 @@ +package discovery + +import ( + "net" + + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/events" + "github.com/gnolang/gno/tm2/pkg/p2p/types" +) + +type ( + broadcastDelegate func(byte, []byte) + peersDelegate func() p2p.PeerSet + stopPeerForErrorDelegate func(p2p.Peer, error) + dialPeersDelegate func(...*types.NetAddress) + subscribeDelegate func(events.EventFilter) (<-chan events.Event, func()) +) + +type mockSwitch struct { + broadcastFn broadcastDelegate + peersFn peersDelegate + stopPeerForErrorFn stopPeerForErrorDelegate + dialPeersFn dialPeersDelegate + subscribeFn subscribeDelegate +} + +func (m *mockSwitch) Broadcast(chID byte, data []byte) { + if m.broadcastFn != nil { + m.broadcastFn(chID, data) + } +} + +func (m *mockSwitch) Peers() p2p.PeerSet { + if m.peersFn != nil { + return m.peersFn() + } + + return nil +} + +func (m *mockSwitch) StopPeerForError(peer p2p.Peer, err error) { + if m.stopPeerForErrorFn != nil { + m.stopPeerForErrorFn(peer, err) + } +} + +func (m *mockSwitch) DialPeers(peerAddrs ...*types.NetAddress) { + if m.dialPeersFn != nil { + m.dialPeersFn(peerAddrs...) + } +} + +func (m *mockSwitch) Subscribe(filter events.EventFilter) (<-chan events.Event, func()) { + if m.subscribeFn != nil { + m.subscribeFn(filter) + } + + return nil, func() {} +} + +type ( + addDelegate func(p2p.Peer) + removeDelegate func(types.ID) bool + hasDelegate func(types.ID) bool + hasIPDelegate func(net.IP) bool + getPeerDelegate func(types.ID) p2p.Peer + listDelegate func() []p2p.Peer + numInboundDelegate func() uint64 + numOutboundDelegate func() uint64 +) + +type mockPeerSet struct { + addFn addDelegate + removeFn removeDelegate + hasFn hasDelegate + hasIPFn hasIPDelegate + getFn getPeerDelegate + listFn listDelegate + numInboundFn numInboundDelegate + numOutboundFn numOutboundDelegate +} + +func (m *mockPeerSet) Add(peer p2p.Peer) { + if m.addFn != nil { + m.addFn(peer) + } +} + +func (m *mockPeerSet) Remove(key types.ID) bool { + if m.removeFn != nil { + m.removeFn(key) + } + + return false +} + +func (m *mockPeerSet) Has(key types.ID) bool { + if m.hasFn != nil { + return m.hasFn(key) + } + + return false +} + +func (m *mockPeerSet) Get(key types.ID) p2p.Peer { + if m.getFn != nil { + return m.getFn(key) + } + + return nil +} + +func (m *mockPeerSet) List() []p2p.Peer { + if m.listFn != nil { + return m.listFn() + } + + return nil +} + +func (m *mockPeerSet) NumInbound() uint64 { + if m.numInboundFn != nil { + return m.numInboundFn() + } + + return 0 +} + +func (m *mockPeerSet) NumOutbound() uint64 { + if m.numOutboundFn != nil { + return m.numOutboundFn() + } + + return 0 +} diff --git a/tm2/pkg/p2p/discovery/option.go b/tm2/pkg/p2p/discovery/option.go new file mode 100644 index 00000000000..dc0fb95b109 --- /dev/null +++ b/tm2/pkg/p2p/discovery/option.go @@ -0,0 +1,12 @@ +package discovery + +import "time" + +type Option func(*Reactor) + +// WithDiscoveryInterval sets the discovery crawl interval +func WithDiscoveryInterval(interval time.Duration) Option { + return func(r *Reactor) { + r.discoveryInterval = interval + } +} diff --git a/tm2/pkg/p2p/discovery/package.go b/tm2/pkg/p2p/discovery/package.go new file mode 100644 index 00000000000..a3865fdf5d2 --- /dev/null +++ b/tm2/pkg/p2p/discovery/package.go @@ -0,0 +1,16 @@ +package discovery + +import ( + "github.com/gnolang/gno/tm2/pkg/amino" +) + +var Package = amino.RegisterPackage(amino.NewPackage( + "github.com/gnolang/gno/tm2/pkg/p2p/discovery", + "p2p", + amino.GetCallersDirname(), +). + WithTypes( + &Request{}, + &Response{}, + ), +) diff --git a/tm2/pkg/p2p/discovery/types.go b/tm2/pkg/p2p/discovery/types.go new file mode 100644 index 00000000000..87ea936ebb5 --- /dev/null +++ b/tm2/pkg/p2p/discovery/types.go @@ -0,0 +1,44 @@ +package discovery + +import ( + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/p2p/types" +) + +var errNoPeers = errors.New("no peers received") + +// Message is the wrapper for the discovery message +type Message interface { + ValidateBasic() error +} + +// Request is the peer discovery request. +// It is empty by design, since it's used as +// a notification type +type Request struct{} + +func (r *Request) ValidateBasic() error { + return nil +} + +// Response is the peer discovery response +type Response struct { + Peers []*types.NetAddress // the peer set returned by the peer +} + +func (r *Response) ValidateBasic() error { + // Make sure at least some peers were received + if len(r.Peers) == 0 { + return errNoPeers + } + + // Make sure the returned peer dial + // addresses are valid + for _, peer := range r.Peers { + if err := peer.Validate(); err != nil { + return err + } + } + + return nil +} diff --git a/tm2/pkg/p2p/discovery/types_test.go b/tm2/pkg/p2p/discovery/types_test.go new file mode 100644 index 00000000000..0ac2c16f4e5 --- /dev/null +++ b/tm2/pkg/p2p/discovery/types_test.go @@ -0,0 +1,80 @@ +package discovery + +import ( + "net" + "testing" + + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateNetAddrs generates random net addresses +func generateNetAddrs(t *testing.T, count int) []*types.NetAddress { + t.Helper() + + addrs := make([]*types.NetAddress, count) + + for i := 0; i < count; i++ { + var ( + key = types.GenerateNodeKey() + address = "127.0.0.1:8080" + ) + + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + require.NoError(t, err) + + addr, err := types.NewNetAddress(key.ID(), tcpAddr) + require.NoError(t, err) + + addrs[i] = addr + } + + return addrs +} + +func TestRequest_ValidateBasic(t *testing.T) { + t.Parallel() + + r := &Request{} + + assert.NoError(t, r.ValidateBasic()) +} + +func TestResponse_ValidateBasic(t *testing.T) { + t.Parallel() + + t.Run("empty peer set", func(t *testing.T) { + t.Parallel() + + r := &Response{ + Peers: make([]*types.NetAddress, 0), + } + + assert.ErrorIs(t, r.ValidateBasic(), errNoPeers) + }) + + t.Run("invalid peer dial address", func(t *testing.T) { + t.Parallel() + + r := &Response{ + Peers: []*types.NetAddress{ + { + ID: "", // invalid ID + }, + }, + } + + assert.Error(t, r.ValidateBasic()) + }) + + t.Run("valid peer set", func(t *testing.T) { + t.Parallel() + + r := &Response{ + Peers: generateNetAddrs(t, 10), + } + + assert.NoError(t, r.ValidateBasic()) + }) +} diff --git a/tm2/pkg/p2p/errors.go b/tm2/pkg/p2p/errors.go deleted file mode 100644 index d4ad58e8ab5..00000000000 --- a/tm2/pkg/p2p/errors.go +++ /dev/null @@ -1,184 +0,0 @@ -package p2p - -import ( - "fmt" - "net" -) - -// FilterTimeoutError indicates that a filter operation timed out. -type FilterTimeoutError struct{} - -func (e FilterTimeoutError) Error() string { - return "filter timed out" -} - -// RejectedError indicates that a Peer was rejected carrying additional -// information as to the reason. -type RejectedError struct { - addr NetAddress - conn net.Conn - err error - id ID - isAuthFailure bool - isDuplicate bool - isFiltered bool - isIncompatible bool - isNodeInfoInvalid bool - isSelf bool -} - -// Addr returns the NetAddress for the rejected Peer. -func (e RejectedError) Addr() NetAddress { - return e.addr -} - -func (e RejectedError) Error() string { - if e.isAuthFailure { - return fmt.Sprintf("auth failure: %s", e.err) - } - - if e.isDuplicate { - if e.conn != nil { - return fmt.Sprintf( - "duplicate CONN<%s>", - e.conn.RemoteAddr().String(), - ) - } - if !e.id.IsZero() { - return fmt.Sprintf("duplicate ID<%v>", e.id) - } - } - - if e.isFiltered { - if e.conn != nil { - return fmt.Sprintf( - "filtered CONN<%s>: %s", - e.conn.RemoteAddr().String(), - e.err, - ) - } - - if !e.id.IsZero() { - return fmt.Sprintf("filtered ID<%v>: %s", e.id, e.err) - } - } - - if e.isIncompatible { - return fmt.Sprintf("incompatible: %s", e.err) - } - - if e.isNodeInfoInvalid { - return fmt.Sprintf("invalid NodeInfo: %s", e.err) - } - - if e.isSelf { - return fmt.Sprintf("self ID<%v>", e.id) - } - - return fmt.Sprintf("%s", e.err) -} - -// IsAuthFailure when Peer authentication was unsuccessful. -func (e RejectedError) IsAuthFailure() bool { return e.isAuthFailure } - -// IsDuplicate when Peer ID or IP are present already. -func (e RejectedError) IsDuplicate() bool { return e.isDuplicate } - -// IsFiltered when Peer ID or IP was filtered. -func (e RejectedError) IsFiltered() bool { return e.isFiltered } - -// IsIncompatible when Peer NodeInfo is not compatible with our own. -func (e RejectedError) IsIncompatible() bool { return e.isIncompatible } - -// IsNodeInfoInvalid when the sent NodeInfo is not valid. -func (e RejectedError) IsNodeInfoInvalid() bool { return e.isNodeInfoInvalid } - -// IsSelf when Peer is our own node. -func (e RejectedError) IsSelf() bool { return e.isSelf } - -// SwitchDuplicatePeerIDError to be raised when a peer is connecting with a known -// ID. -type SwitchDuplicatePeerIDError struct { - ID ID -} - -func (e SwitchDuplicatePeerIDError) Error() string { - return fmt.Sprintf("duplicate peer ID %v", e.ID) -} - -// SwitchDuplicatePeerIPError to be raised when a peer is connecting with a known -// IP. -type SwitchDuplicatePeerIPError struct { - IP net.IP -} - -func (e SwitchDuplicatePeerIPError) Error() string { - return fmt.Sprintf("duplicate peer IP %v", e.IP.String()) -} - -// SwitchConnectToSelfError to be raised when trying to connect to itself. -type SwitchConnectToSelfError struct { - Addr *NetAddress -} - -func (e SwitchConnectToSelfError) Error() string { - return fmt.Sprintf("connect to self: %v", e.Addr) -} - -type SwitchAuthenticationFailureError struct { - Dialed *NetAddress - Got ID -} - -func (e SwitchAuthenticationFailureError) Error() string { - return fmt.Sprintf( - "failed to authenticate peer. Dialed %v, but got peer with ID %s", - e.Dialed, - e.Got, - ) -} - -// TransportClosedError is raised when the Transport has been closed. -type TransportClosedError struct{} - -func (e TransportClosedError) Error() string { - return "transport has been closed" -} - -// ------------------------------------------------------------------- - -type NetAddressNoIDError struct { - Addr string -} - -func (e NetAddressNoIDError) Error() string { - return fmt.Sprintf("address (%s) does not contain ID", e.Addr) -} - -type NetAddressInvalidError struct { - Addr string - Err error -} - -func (e NetAddressInvalidError) Error() string { - return fmt.Sprintf("invalid address (%s): %v", e.Addr, e.Err) -} - -type NetAddressLookupError struct { - Addr string - Err error -} - -func (e NetAddressLookupError) Error() string { - return fmt.Sprintf("error looking up host (%s): %v", e.Addr, e.Err) -} - -// CurrentlyDialingOrExistingAddressError indicates that we're currently -// dialing this address or it belongs to an existing peer. -type CurrentlyDialingOrExistingAddressError struct { - Addr string -} - -func (e CurrentlyDialingOrExistingAddressError) Error() string { - return fmt.Sprintf("connection with %s has been established or dialed", e.Addr) -} diff --git a/tm2/pkg/p2p/events/doc.go b/tm2/pkg/p2p/events/doc.go new file mode 100644 index 00000000000..a624102379e --- /dev/null +++ b/tm2/pkg/p2p/events/doc.go @@ -0,0 +1,3 @@ +// Package events contains a simple p2p event system implementation, that simplifies asynchronous event flows in the +// p2p module. The event subscriptions allow for event filtering, which eases the load on the event notification flow. +package events diff --git a/tm2/pkg/p2p/events/events.go b/tm2/pkg/p2p/events/events.go new file mode 100644 index 00000000000..bf76e27d91e --- /dev/null +++ b/tm2/pkg/p2p/events/events.go @@ -0,0 +1,104 @@ +package events + +import ( + "sync" + + "github.com/rs/xid" +) + +// EventFilter is the filter function used to +// filter incoming p2p events. A false flag will +// consider the event as irrelevant +type EventFilter func(Event) bool + +// Events is the p2p event switch +type Events struct { + subs subscriptions + subscriptionsMux sync.RWMutex +} + +// New creates a new event subscription manager +func New() *Events { + return &Events{ + subs: make(subscriptions), + } +} + +// Subscribe registers a new filtered event listener +func (es *Events) Subscribe(filterFn EventFilter) (<-chan Event, func()) { + es.subscriptionsMux.Lock() + defer es.subscriptionsMux.Unlock() + + // Create a new subscription + id, ch := es.subs.add(filterFn) + + // Create the unsubscribe callback + unsubscribeFn := func() { + es.subscriptionsMux.Lock() + defer es.subscriptionsMux.Unlock() + + es.subs.remove(id) + } + + return ch, unsubscribeFn +} + +// Notify notifies all subscribers of an incoming event [BLOCKING] +func (es *Events) Notify(event Event) { + es.subscriptionsMux.RLock() + defer es.subscriptionsMux.RUnlock() + + es.subs.notify(event) +} + +type ( + // subscriptions holds the corresponding subscription information + subscriptions map[string]subscription // subscription ID -> subscription + + // subscription wraps the subscription notification channel, + // and the event filter + subscription struct { + ch chan Event + filterFn EventFilter + } +) + +// add adds a new subscription to the subscription map. +// Returns the subscription ID, and update channel +func (s *subscriptions) add(filterFn EventFilter) (string, chan Event) { + var ( + id = xid.New().String() + ch = make(chan Event, 1) + ) + + (*s)[id] = subscription{ + ch: ch, + filterFn: filterFn, + } + + return id, ch +} + +// remove removes the given subscription +func (s *subscriptions) remove(id string) { + if sub, exists := (*s)[id]; exists { + // Close the notification channel + close(sub.ch) + } + + // Delete the subscription + delete(*s, id) +} + +// notify notifies all subscription listeners, +// if their filters pass +func (s *subscriptions) notify(event Event) { + // Notify the listeners + for _, sub := range *s { + if !sub.filterFn(event) { + continue + } + + sub.ch <- event + } +} diff --git a/tm2/pkg/p2p/events/events_test.go b/tm2/pkg/p2p/events/events_test.go new file mode 100644 index 00000000000..a0feafceddb --- /dev/null +++ b/tm2/pkg/p2p/events/events_test.go @@ -0,0 +1,94 @@ +package events + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateEvents generates p2p events +func generateEvents(count int) []Event { + events := make([]Event, 0, count) + + for i := range count { + var event Event + + if i%2 == 0 { + event = PeerConnectedEvent{ + PeerID: types.ID(fmt.Sprintf("peer-%d", i)), + } + } else { + event = PeerDisconnectedEvent{ + PeerID: types.ID(fmt.Sprintf("peer-%d", i)), + } + } + + events = append(events, event) + } + + return events +} + +func TestEvents_Subscribe(t *testing.T) { + t.Parallel() + + var ( + capturedEvents []Event + + events = generateEvents(10) + subFn = func(e Event) bool { + return e.Type() == PeerDisconnected + } + ) + + // Create the events manager + e := New() + + // Subscribe to events + ch, unsubFn := e.Subscribe(subFn) + defer unsubFn() + + // Listen for the events + var wg sync.WaitGroup + + wg.Add(1) + + go func() { + defer wg.Done() + + timeout := time.After(5 * time.Second) + + for { + select { + case ev := <-ch: + capturedEvents = append(capturedEvents, ev) + + if len(capturedEvents) == len(events)/2 { + return + } + case <-timeout: + return + } + } + }() + + // Send out the events + for _, ev := range events { + e.Notify(ev) + } + + wg.Wait() + + // Make sure the events were captured + // and filtered properly + require.Len(t, capturedEvents, len(events)/2) + + for _, ev := range capturedEvents { + assert.Equal(t, ev.Type(), PeerDisconnected) + } +} diff --git a/tm2/pkg/p2p/events/types.go b/tm2/pkg/p2p/events/types.go new file mode 100644 index 00000000000..cbaac1816ff --- /dev/null +++ b/tm2/pkg/p2p/events/types.go @@ -0,0 +1,39 @@ +package events + +import ( + "net" + + "github.com/gnolang/gno/tm2/pkg/p2p/types" +) + +type EventType string + +const ( + PeerConnected EventType = "PeerConnected" // emitted when a fresh peer connects + PeerDisconnected EventType = "PeerDisconnected" // emitted when a peer disconnects +) + +// Event is a generic p2p event +type Event interface { + // Type returns the type information for the event + Type() EventType +} + +type PeerConnectedEvent struct { + PeerID types.ID // the ID of the peer + Address net.Addr // the remote address of the peer +} + +func (p PeerConnectedEvent) Type() EventType { + return PeerConnected +} + +type PeerDisconnectedEvent struct { + PeerID types.ID // the ID of the peer + Address net.Addr // the remote address of the peer + Reason error // the disconnect reason, if any +} + +func (p PeerDisconnectedEvent) Type() EventType { + return PeerDisconnected +} diff --git a/tm2/pkg/p2p/fuzz.go b/tm2/pkg/p2p/fuzz.go deleted file mode 100644 index 03cf88cf750..00000000000 --- a/tm2/pkg/p2p/fuzz.go +++ /dev/null @@ -1,131 +0,0 @@ -package p2p - -import ( - "net" - "sync" - "time" - - "github.com/gnolang/gno/tm2/pkg/p2p/config" - "github.com/gnolang/gno/tm2/pkg/random" -) - -// FuzzedConnection wraps any net.Conn and depending on the mode either delays -// reads/writes or randomly drops reads/writes/connections. -type FuzzedConnection struct { - conn net.Conn - - mtx sync.Mutex - start <-chan time.Time - active bool - - config *config.FuzzConnConfig -} - -// FuzzConnAfterFromConfig creates a new FuzzedConnection from a config. -// Fuzzing starts when the duration elapses. -func FuzzConnAfterFromConfig( - conn net.Conn, - d time.Duration, - config *config.FuzzConnConfig, -) net.Conn { - return &FuzzedConnection{ - conn: conn, - start: time.After(d), - active: false, - config: config, - } -} - -// Config returns the connection's config. -func (fc *FuzzedConnection) Config() *config.FuzzConnConfig { - return fc.config -} - -// Read implements net.Conn. -func (fc *FuzzedConnection) Read(data []byte) (n int, err error) { - if fc.fuzz() { - return 0, nil - } - return fc.conn.Read(data) -} - -// Write implements net.Conn. -func (fc *FuzzedConnection) Write(data []byte) (n int, err error) { - if fc.fuzz() { - return 0, nil - } - return fc.conn.Write(data) -} - -// Close implements net.Conn. -func (fc *FuzzedConnection) Close() error { return fc.conn.Close() } - -// LocalAddr implements net.Conn. -func (fc *FuzzedConnection) LocalAddr() net.Addr { return fc.conn.LocalAddr() } - -// RemoteAddr implements net.Conn. -func (fc *FuzzedConnection) RemoteAddr() net.Addr { return fc.conn.RemoteAddr() } - -// SetDeadline implements net.Conn. -func (fc *FuzzedConnection) SetDeadline(t time.Time) error { return fc.conn.SetDeadline(t) } - -// SetReadDeadline implements net.Conn. -func (fc *FuzzedConnection) SetReadDeadline(t time.Time) error { - return fc.conn.SetReadDeadline(t) -} - -// SetWriteDeadline implements net.Conn. -func (fc *FuzzedConnection) SetWriteDeadline(t time.Time) error { - return fc.conn.SetWriteDeadline(t) -} - -func (fc *FuzzedConnection) randomDuration() time.Duration { - maxDelayMillis := int(fc.config.MaxDelay.Nanoseconds() / 1000) - return time.Millisecond * time.Duration(random.RandInt()%maxDelayMillis) //nolint: gas -} - -// implements the fuzz (delay, kill conn) -// and returns whether or not the read/write should be ignored -func (fc *FuzzedConnection) fuzz() bool { - if !fc.shouldFuzz() { - return false - } - - switch fc.config.Mode { - case config.FuzzModeDrop: - // randomly drop the r/w, drop the conn, or sleep - r := random.RandFloat64() - switch { - case r <= fc.config.ProbDropRW: - return true - case r < fc.config.ProbDropRW+fc.config.ProbDropConn: - // XXX: can't this fail because machine precision? - // XXX: do we need an error? - fc.Close() //nolint: errcheck, gas - return true - case r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep: - time.Sleep(fc.randomDuration()) - } - case config.FuzzModeDelay: - // sleep a bit - time.Sleep(fc.randomDuration()) - } - return false -} - -func (fc *FuzzedConnection) shouldFuzz() bool { - if fc.active { - return true - } - - fc.mtx.Lock() - defer fc.mtx.Unlock() - - select { - case <-fc.start: - fc.active = true - return true - default: - return false - } -} diff --git a/tm2/pkg/p2p/key.go b/tm2/pkg/p2p/key.go deleted file mode 100644 index a41edeb07f8..00000000000 --- a/tm2/pkg/p2p/key.go +++ /dev/null @@ -1,94 +0,0 @@ -package p2p - -import ( - "bytes" - "fmt" - "os" - - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - osm "github.com/gnolang/gno/tm2/pkg/os" -) - -// ------------------------------------------------------------------------------ -// Persistent peer ID -// TODO: encrypt on disk - -// NodeKey is the persistent peer key. -// It contains the nodes private key for authentication. -// NOTE: keep in sync with gno.land/cmd/gnoland/secrets.go -type NodeKey struct { - crypto.PrivKey `json:"priv_key"` // our priv key -} - -func (nk NodeKey) ID() ID { - return nk.PubKey().Address().ID() -} - -// LoadOrGenNodeKey attempts to load the NodeKey from the given filePath. -// If the file does not exist, it generates and saves a new NodeKey. -func LoadOrGenNodeKey(filePath string) (*NodeKey, error) { - if osm.FileExists(filePath) { - nodeKey, err := LoadNodeKey(filePath) - if err != nil { - return nil, err - } - return nodeKey, nil - } - return genNodeKey(filePath) -} - -func LoadNodeKey(filePath string) (*NodeKey, error) { - jsonBytes, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - nodeKey := new(NodeKey) - err = amino.UnmarshalJSON(jsonBytes, nodeKey) - if err != nil { - return nil, fmt.Errorf("Error reading NodeKey from %v: %w", filePath, err) - } - return nodeKey, nil -} - -func genNodeKey(filePath string) (*NodeKey, error) { - privKey := ed25519.GenPrivKey() - nodeKey := &NodeKey{ - PrivKey: privKey, - } - - jsonBytes, err := amino.MarshalJSON(nodeKey) - if err != nil { - return nil, err - } - err = os.WriteFile(filePath, jsonBytes, 0o600) - if err != nil { - return nil, err - } - return nodeKey, nil -} - -// ------------------------------------------------------------------------------ - -// MakePoWTarget returns the big-endian encoding of 2^(targetBits - difficulty) - 1. -// It can be used as a Proof of Work target. -// NOTE: targetBits must be a multiple of 8 and difficulty must be less than targetBits. -func MakePoWTarget(difficulty, targetBits uint) []byte { - if targetBits%8 != 0 { - panic(fmt.Sprintf("targetBits (%d) not a multiple of 8", targetBits)) - } - if difficulty >= targetBits { - panic(fmt.Sprintf("difficulty (%d) >= targetBits (%d)", difficulty, targetBits)) - } - targetBytes := targetBits / 8 - zeroPrefixLen := (int(difficulty) / 8) - prefix := bytes.Repeat([]byte{0}, zeroPrefixLen) - mod := (difficulty % 8) - if mod > 0 { - nonZeroPrefix := byte(1<<(8-mod) - 1) - prefix = append(prefix, nonZeroPrefix) - } - tailLen := int(targetBytes) - len(prefix) - return append(prefix, bytes.Repeat([]byte{0xFF}, tailLen)...) -} diff --git a/tm2/pkg/p2p/key_test.go b/tm2/pkg/p2p/key_test.go deleted file mode 100644 index 4f67cc0a5da..00000000000 --- a/tm2/pkg/p2p/key_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package p2p - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "github.com/gnolang/gno/tm2/pkg/random" - "github.com/stretchr/testify/assert" -) - -func TestLoadOrGenNodeKey(t *testing.T) { - t.Parallel() - - filePath := filepath.Join(os.TempDir(), random.RandStr(12)+"_peer_id.json") - - nodeKey, err := LoadOrGenNodeKey(filePath) - assert.Nil(t, err) - - nodeKey2, err := LoadOrGenNodeKey(filePath) - assert.Nil(t, err) - - assert.Equal(t, nodeKey, nodeKey2) -} - -// ---------------------------------------------------------- - -func padBytes(bz []byte, targetBytes int) []byte { - return append(bz, bytes.Repeat([]byte{0xFF}, targetBytes-len(bz))...) -} - -func TestPoWTarget(t *testing.T) { - t.Parallel() - - targetBytes := 20 - cases := []struct { - difficulty uint - target []byte - }{ - {0, padBytes([]byte{}, targetBytes)}, - {1, padBytes([]byte{127}, targetBytes)}, - {8, padBytes([]byte{0}, targetBytes)}, - {9, padBytes([]byte{0, 127}, targetBytes)}, - {10, padBytes([]byte{0, 63}, targetBytes)}, - {16, padBytes([]byte{0, 0}, targetBytes)}, - {17, padBytes([]byte{0, 0, 127}, targetBytes)}, - } - - for _, c := range cases { - assert.Equal(t, MakePoWTarget(c.difficulty, 20*8), c.target) - } -} diff --git a/tm2/pkg/p2p/mock/peer.go b/tm2/pkg/p2p/mock/peer.go index 906c168c3a8..e5a01952831 100644 --- a/tm2/pkg/p2p/mock/peer.go +++ b/tm2/pkg/p2p/mock/peer.go @@ -1,68 +1,377 @@ package mock import ( + "fmt" + "io" + "log/slog" "net" + "testing" + "time" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/service" + "github.com/stretchr/testify/require" ) +type ( + flushStopDelegate func() + idDelegate func() types.ID + remoteIPDelegate func() net.IP + remoteAddrDelegate func() net.Addr + isOutboundDelegate func() bool + isPersistentDelegate func() bool + isPrivateDelegate func() bool + closeConnDelegate func() error + nodeInfoDelegate func() types.NodeInfo + statusDelegate func() conn.ConnectionStatus + socketAddrDelegate func() *types.NetAddress + sendDelegate func(byte, []byte) bool + trySendDelegate func(byte, []byte) bool + setDelegate func(string, any) + getDelegate func(string) any + stopDelegate func() error +) + +// GeneratePeers generates random peers +func GeneratePeers(t *testing.T, count int) []*Peer { + t.Helper() + + peers := make([]*Peer, count) + + for i := range count { + var ( + key = types.GenerateNodeKey() + address = "127.0.0.1:8080" + ) + + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + require.NoError(t, err) + + addr, err := types.NewNetAddress(key.ID(), tcpAddr) + require.NoError(t, err) + + p := &Peer{ + IDFn: func() types.ID { + return key.ID() + }, + NodeInfoFn: func() types.NodeInfo { + return types.NodeInfo{ + PeerID: key.ID(), + } + }, + SocketAddrFn: func() *types.NetAddress { + return addr + }, + } + + p.BaseService = *service.NewBaseService( + slog.New(slog.NewTextHandler(io.Discard, nil)), + fmt.Sprintf("peer-%d", i), + p, + ) + + peers[i] = p + } + + return peers +} + type Peer struct { - *service.BaseService - ip net.IP - id p2p.ID - addr *p2p.NetAddress - kv map[string]interface{} - Outbound, Persistent bool -} - -// NewPeer creates and starts a new mock peer. If the ip -// is nil, random routable address is used. -func NewPeer(ip net.IP) *Peer { - var netAddr *p2p.NetAddress - if ip == nil { - _, netAddr = p2p.CreateRoutableAddr() - } else { - netAddr = p2p.NewNetAddressFromIPPort("", ip, 26656) - } - nodeKey := p2p.NodeKey{PrivKey: ed25519.GenPrivKey()} - netAddr.ID = nodeKey.ID() - mp := &Peer{ - ip: ip, - id: nodeKey.ID(), - addr: netAddr, - kv: make(map[string]interface{}), - } - mp.BaseService = service.NewBaseService(nil, "MockPeer", mp) - mp.Start() - return mp -} - -func (mp *Peer) FlushStop() { mp.Stop() } -func (mp *Peer) TrySend(chID byte, msgBytes []byte) bool { return true } -func (mp *Peer) Send(chID byte, msgBytes []byte) bool { return true } -func (mp *Peer) NodeInfo() p2p.NodeInfo { - return p2p.NodeInfo{ - NetAddress: mp.addr, - } -} -func (mp *Peer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } -func (mp *Peer) ID() p2p.ID { return mp.id } -func (mp *Peer) IsOutbound() bool { return mp.Outbound } -func (mp *Peer) IsPersistent() bool { return mp.Persistent } -func (mp *Peer) Get(key string) interface{} { - if value, ok := mp.kv[key]; ok { - return value + service.BaseService + + FlushStopFn flushStopDelegate + IDFn idDelegate + RemoteIPFn remoteIPDelegate + RemoteAddrFn remoteAddrDelegate + IsOutboundFn isOutboundDelegate + IsPersistentFn isPersistentDelegate + IsPrivateFn isPrivateDelegate + CloseConnFn closeConnDelegate + NodeInfoFn nodeInfoDelegate + StopFn stopDelegate + StatusFn statusDelegate + SocketAddrFn socketAddrDelegate + SendFn sendDelegate + TrySendFn trySendDelegate + SetFn setDelegate + GetFn getDelegate +} + +func (m *Peer) FlushStop() { + if m.FlushStopFn != nil { + m.FlushStopFn() + } +} + +func (m *Peer) ID() types.ID { + if m.IDFn != nil { + return m.IDFn() + } + + return "" +} + +func (m *Peer) RemoteIP() net.IP { + if m.RemoteIPFn != nil { + return m.RemoteIPFn() + } + + return nil +} + +func (m *Peer) RemoteAddr() net.Addr { + if m.RemoteAddrFn != nil { + return m.RemoteAddrFn() + } + + return nil +} + +func (m *Peer) Stop() error { + if m.StopFn != nil { + return m.StopFn() + } + + return nil +} + +func (m *Peer) IsOutbound() bool { + if m.IsOutboundFn != nil { + return m.IsOutboundFn() + } + + return false +} + +func (m *Peer) IsPersistent() bool { + if m.IsPersistentFn != nil { + return m.IsPersistentFn() + } + + return false +} + +func (m *Peer) IsPrivate() bool { + if m.IsPrivateFn != nil { + return m.IsPrivateFn() + } + + return false +} + +func (m *Peer) CloseConn() error { + if m.CloseConnFn != nil { + return m.CloseConnFn() + } + + return nil +} + +func (m *Peer) NodeInfo() types.NodeInfo { + if m.NodeInfoFn != nil { + return m.NodeInfoFn() + } + + return types.NodeInfo{} +} + +func (m *Peer) Status() conn.ConnectionStatus { + if m.StatusFn != nil { + return m.StatusFn() + } + + return conn.ConnectionStatus{} +} + +func (m *Peer) SocketAddr() *types.NetAddress { + if m.SocketAddrFn != nil { + return m.SocketAddrFn() + } + + return nil +} + +func (m *Peer) Send(classifier byte, data []byte) bool { + if m.SendFn != nil { + return m.SendFn(classifier, data) + } + + return false +} + +func (m *Peer) TrySend(classifier byte, data []byte) bool { + if m.TrySendFn != nil { + return m.TrySendFn(classifier, data) + } + + return false +} + +func (m *Peer) Set(key string, data any) { + if m.SetFn != nil { + m.SetFn(key, data) + } +} + +func (m *Peer) Get(key string) any { + if m.GetFn != nil { + return m.GetFn(key) + } + + return nil +} + +type ( + readDelegate func([]byte) (int, error) + writeDelegate func([]byte) (int, error) + closeDelegate func() error + localAddrDelegate func() net.Addr + setDeadlineDelegate func(time.Time) error +) + +type Conn struct { + ReadFn readDelegate + WriteFn writeDelegate + CloseFn closeDelegate + LocalAddrFn localAddrDelegate + RemoteAddrFn remoteAddrDelegate + SetDeadlineFn setDeadlineDelegate + SetReadDeadlineFn setDeadlineDelegate + SetWriteDeadlineFn setDeadlineDelegate +} + +func (m *Conn) Read(b []byte) (int, error) { + if m.ReadFn != nil { + return m.ReadFn(b) + } + + return 0, nil +} + +func (m *Conn) Write(b []byte) (int, error) { + if m.WriteFn != nil { + return m.WriteFn(b) } + + return 0, nil +} + +func (m *Conn) Close() error { + if m.CloseFn != nil { + return m.CloseFn() + } + return nil } -func (mp *Peer) Set(key string, value interface{}) { - mp.kv[key] = value +func (m *Conn) LocalAddr() net.Addr { + if m.LocalAddrFn != nil { + return m.LocalAddrFn() + } + + return nil +} + +func (m *Conn) RemoteAddr() net.Addr { + if m.RemoteAddrFn != nil { + return m.RemoteAddrFn() + } + + return nil +} + +func (m *Conn) SetDeadline(t time.Time) error { + if m.SetDeadlineFn != nil { + return m.SetDeadlineFn(t) + } + + return nil +} + +func (m *Conn) SetReadDeadline(t time.Time) error { + if m.SetReadDeadlineFn != nil { + return m.SetReadDeadlineFn(t) + } + + return nil +} + +func (m *Conn) SetWriteDeadline(t time.Time) error { + if m.SetWriteDeadlineFn != nil { + return m.SetWriteDeadlineFn(t) + } + + return nil +} + +type ( + startDelegate func() error + stringDelegate func() string +) + +type MConn struct { + FlushFn flushStopDelegate + StartFn startDelegate + StopFn stopDelegate + SendFn sendDelegate + TrySendFn trySendDelegate + StatusFn statusDelegate + StringFn stringDelegate +} + +func (m *MConn) FlushStop() { + if m.FlushFn != nil { + m.FlushFn() + } +} + +func (m *MConn) Start() error { + if m.StartFn != nil { + return m.StartFn() + } + + return nil +} + +func (m *MConn) Stop() error { + if m.StopFn != nil { + return m.StopFn() + } + + return nil +} + +func (m *MConn) Send(ch byte, data []byte) bool { + if m.SendFn != nil { + return m.SendFn(ch, data) + } + + return false +} + +func (m *MConn) TrySend(ch byte, data []byte) bool { + if m.TrySendFn != nil { + return m.TrySendFn(ch, data) + } + + return false +} + +func (m *MConn) SetLogger(_ *slog.Logger) {} + +func (m *MConn) Status() conn.ConnectionStatus { + if m.StatusFn != nil { + return m.StatusFn() + } + + return conn.ConnectionStatus{} +} + +func (m *MConn) String() string { + if m.StringFn != nil { + return m.StringFn() + } + + return "" } -func (mp *Peer) RemoteIP() net.IP { return mp.ip } -func (mp *Peer) SocketAddr() *p2p.NetAddress { return mp.addr } -func (mp *Peer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: mp.ip, Port: 8800} } -func (mp *Peer) CloseConn() error { return nil } diff --git a/tm2/pkg/p2p/mock/reactor.go b/tm2/pkg/p2p/mock/reactor.go deleted file mode 100644 index fe123fdc0b2..00000000000 --- a/tm2/pkg/p2p/mock/reactor.go +++ /dev/null @@ -1,23 +0,0 @@ -package mock - -import ( - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p" - "github.com/gnolang/gno/tm2/pkg/p2p/conn" -) - -type Reactor struct { - p2p.BaseReactor -} - -func NewReactor() *Reactor { - r := &Reactor{} - r.BaseReactor = *p2p.NewBaseReactor("Reactor", r) - r.SetLogger(log.NewNoopLogger()) - return r -} - -func (r *Reactor) GetChannels() []*conn.ChannelDescriptor { return []*conn.ChannelDescriptor{} } -func (r *Reactor) AddPeer(peer p2p.Peer) {} -func (r *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) {} -func (r *Reactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) {} diff --git a/tm2/pkg/p2p/mock_test.go b/tm2/pkg/p2p/mock_test.go new file mode 100644 index 00000000000..d870e3f8133 --- /dev/null +++ b/tm2/pkg/p2p/mock_test.go @@ -0,0 +1,404 @@ +package p2p + +import ( + "context" + "log/slog" + "net" + "time" + + "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/types" +) + +type ( + netAddressDelegate func() types.NetAddress + acceptDelegate func(context.Context, PeerBehavior) (Peer, error) + dialDelegate func(context.Context, types.NetAddress, PeerBehavior) (Peer, error) + removeDelegate func(Peer) +) + +type mockTransport struct { + netAddressFn netAddressDelegate + acceptFn acceptDelegate + dialFn dialDelegate + removeFn removeDelegate +} + +func (m *mockTransport) NetAddress() types.NetAddress { + if m.netAddressFn != nil { + return m.netAddressFn() + } + + return types.NetAddress{} +} + +func (m *mockTransport) Accept(ctx context.Context, behavior PeerBehavior) (Peer, error) { + if m.acceptFn != nil { + return m.acceptFn(ctx, behavior) + } + + return nil, nil +} + +func (m *mockTransport) Dial(ctx context.Context, address types.NetAddress, behavior PeerBehavior) (Peer, error) { + if m.dialFn != nil { + return m.dialFn(ctx, address, behavior) + } + + return nil, nil +} + +func (m *mockTransport) Remove(p Peer) { + if m.removeFn != nil { + m.removeFn(p) + } +} + +type ( + addDelegate func(Peer) + removePeerDelegate func(types.ID) bool + hasDelegate func(types.ID) bool + hasIPDelegate func(net.IP) bool + getDelegate func(types.ID) Peer + listDelegate func() []Peer + numInboundDelegate func() uint64 + numOutboundDelegate func() uint64 +) + +type mockSet struct { + addFn addDelegate + removeFn removePeerDelegate + hasFn hasDelegate + hasIPFn hasIPDelegate + listFn listDelegate + getFn getDelegate + numInboundFn numInboundDelegate + numOutboundFn numOutboundDelegate +} + +func (m *mockSet) Add(peer Peer) { + if m.addFn != nil { + m.addFn(peer) + } +} + +func (m *mockSet) Remove(key types.ID) bool { + if m.removeFn != nil { + m.removeFn(key) + } + + return false +} + +func (m *mockSet) Has(key types.ID) bool { + if m.hasFn != nil { + return m.hasFn(key) + } + + return false +} + +func (m *mockSet) Get(key types.ID) Peer { + if m.getFn != nil { + return m.getFn(key) + } + + return nil +} + +func (m *mockSet) List() []Peer { + if m.listFn != nil { + return m.listFn() + } + + return nil +} + +func (m *mockSet) NumInbound() uint64 { + if m.numInboundFn != nil { + return m.numInboundFn() + } + + return 0 +} + +func (m *mockSet) NumOutbound() uint64 { + if m.numOutboundFn != nil { + return m.numOutboundFn() + } + + return 0 +} + +type ( + listenerAcceptDelegate func() (net.Conn, error) + closeDelegate func() error + addrDelegate func() net.Addr +) + +type mockListener struct { + acceptFn listenerAcceptDelegate + closeFn closeDelegate + addrFn addrDelegate +} + +func (m *mockListener) Accept() (net.Conn, error) { + if m.acceptFn != nil { + return m.acceptFn() + } + + return nil, nil +} + +func (m *mockListener) Close() error { + if m.closeFn != nil { + return m.closeFn() + } + + return nil +} + +func (m *mockListener) Addr() net.Addr { + if m.addrFn != nil { + return m.addrFn() + } + + return nil +} + +type ( + readDelegate func([]byte) (int, error) + writeDelegate func([]byte) (int, error) + localAddrDelegate func() net.Addr + remoteAddrDelegate func() net.Addr + setDeadlineDelegate func(time.Time) error +) + +type mockConn struct { + readFn readDelegate + writeFn writeDelegate + closeFn closeDelegate + localAddrFn localAddrDelegate + remoteAddrFn remoteAddrDelegate + setDeadlineFn setDeadlineDelegate + setReadDeadlineFn setDeadlineDelegate + setWriteDeadlineFn setDeadlineDelegate +} + +func (m *mockConn) Read(buff []byte) (int, error) { + if m.readFn != nil { + return m.readFn(buff) + } + + return 0, nil +} + +func (m *mockConn) Write(buff []byte) (int, error) { + if m.writeFn != nil { + return m.writeFn(buff) + } + + return 0, nil +} + +func (m *mockConn) Close() error { + if m.closeFn != nil { + return m.closeFn() + } + + return nil +} + +func (m *mockConn) LocalAddr() net.Addr { + if m.localAddrFn != nil { + return m.localAddrFn() + } + + return nil +} + +func (m *mockConn) RemoteAddr() net.Addr { + if m.remoteAddrFn != nil { + return m.remoteAddrFn() + } + + return nil +} + +func (m *mockConn) SetDeadline(t time.Time) error { + if m.setDeadlineFn != nil { + return m.setDeadlineFn(t) + } + + return nil +} + +func (m *mockConn) SetReadDeadline(t time.Time) error { + if m.setReadDeadlineFn != nil { + return m.setReadDeadlineFn(t) + } + + return nil +} + +func (m *mockConn) SetWriteDeadline(t time.Time) error { + if m.setWriteDeadlineFn != nil { + return m.setWriteDeadlineFn(t) + } + + return nil +} + +type ( + startDelegate func() error + onStartDelegate func() error + stopDelegate func() error + onStopDelegate func() + resetDelegate func() error + onResetDelegate func() error + isRunningDelegate func() bool + quitDelegate func() <-chan struct{} + stringDelegate func() string + setLoggerDelegate func(*slog.Logger) + setSwitchDelegate func(Switch) + getChannelsDelegate func() []*conn.ChannelDescriptor + initPeerDelegate func(Peer) + addPeerDelegate func(Peer) + removeSwitchPeerDelegate func(Peer, any) + receiveDelegate func(byte, Peer, []byte) +) + +type mockReactor struct { + startFn startDelegate + onStartFn onStartDelegate + stopFn stopDelegate + onStopFn onStopDelegate + resetFn resetDelegate + onResetFn onResetDelegate + isRunningFn isRunningDelegate + quitFn quitDelegate + stringFn stringDelegate + setLoggerFn setLoggerDelegate + setSwitchFn setSwitchDelegate + getChannelsFn getChannelsDelegate + initPeerFn initPeerDelegate + addPeerFn addPeerDelegate + removePeerFn removeSwitchPeerDelegate + receiveFn receiveDelegate +} + +func (m *mockReactor) Start() error { + if m.startFn != nil { + return m.startFn() + } + + return nil +} + +func (m *mockReactor) OnStart() error { + if m.onStartFn != nil { + return m.onStartFn() + } + + return nil +} + +func (m *mockReactor) Stop() error { + if m.stopFn != nil { + return m.stopFn() + } + + return nil +} + +func (m *mockReactor) OnStop() { + if m.onStopFn != nil { + m.onStopFn() + } +} + +func (m *mockReactor) Reset() error { + if m.resetFn != nil { + return m.resetFn() + } + + return nil +} + +func (m *mockReactor) OnReset() error { + if m.onResetFn != nil { + return m.onResetFn() + } + + return nil +} + +func (m *mockReactor) IsRunning() bool { + if m.isRunningFn != nil { + return m.isRunningFn() + } + + return false +} + +func (m *mockReactor) Quit() <-chan struct{} { + if m.quitFn != nil { + return m.quitFn() + } + + return nil +} + +func (m *mockReactor) String() string { + if m.stringFn != nil { + return m.stringFn() + } + + return "" +} + +func (m *mockReactor) SetLogger(logger *slog.Logger) { + if m.setLoggerFn != nil { + m.setLoggerFn(logger) + } +} + +func (m *mockReactor) SetSwitch(s Switch) { + if m.setSwitchFn != nil { + m.setSwitchFn(s) + } +} + +func (m *mockReactor) GetChannels() []*conn.ChannelDescriptor { + if m.getChannelsFn != nil { + return m.getChannelsFn() + } + + return nil +} + +func (m *mockReactor) InitPeer(peer Peer) Peer { + if m.initPeerFn != nil { + m.initPeerFn(peer) + } + + return nil +} + +func (m *mockReactor) AddPeer(peer Peer) { + if m.addPeerFn != nil { + m.addPeerFn(peer) + } +} + +func (m *mockReactor) RemovePeer(peer Peer, reason any) { + if m.removePeerFn != nil { + m.removePeerFn(peer, reason) + } +} + +func (m *mockReactor) Receive(chID byte, peer Peer, msgBytes []byte) { + if m.receiveFn != nil { + m.receiveFn(chID, peer, msgBytes) + } +} diff --git a/tm2/pkg/p2p/netaddress_test.go b/tm2/pkg/p2p/netaddress_test.go deleted file mode 100644 index 413d020c153..00000000000 --- a/tm2/pkg/p2p/netaddress_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package p2p - -import ( - "encoding/hex" - "net" - "testing" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAddress2ID(t *testing.T) { - t.Parallel() - - idbz, _ := hex.DecodeString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - id := crypto.AddressFromBytes(idbz).ID() - assert.Equal(t, crypto.ID("g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6"), id) - - idbz, _ = hex.DecodeString("deadbeefdeadbeefdeadbeefdeadbeefdead0000") - id = crypto.AddressFromBytes(idbz).ID() - assert.Equal(t, crypto.ID("g1m6kmam774klwlh4dhmhaatd7al026qqqq9c22r"), id) -} - -func TestNewNetAddress(t *testing.T) { - t.Parallel() - - tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") - require.Nil(t, err) - - assert.Panics(t, func() { - NewNetAddress("", tcpAddr) - }) - - idbz, _ := hex.DecodeString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - id := crypto.AddressFromBytes(idbz).ID() - // ^-- is "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6" - - addr := NewNetAddress(id, tcpAddr) - assert.Equal(t, "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", addr.String()) - - assert.NotPanics(t, func() { - NewNetAddress("", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000}) - }, "Calling NewNetAddress with UDPAddr should not panic in testing") -} - -func TestNewNetAddressFromString(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - addr string - expected string - correct bool - }{ - {"no node id and no protocol", "127.0.0.1:8080", "", false}, - {"no node id w/ tcp input", "tcp://127.0.0.1:8080", "", false}, - {"no node id w/ udp input", "udp://127.0.0.1:8080", "", false}, - - {"no protocol", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", true}, - {"tcp input", "tcp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", true}, - {"udp input", "udp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", true}, - {"malformed tcp input", "tcp//g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "", false}, - {"malformed udp input", "udp//g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "", false}, - - // {"127.0.0:8080", false}, - {"invalid host", "notahost", "", false}, - {"invalid port", "127.0.0.1:notapath", "", false}, - {"invalid host w/ port", "notahost:8080", "", false}, - {"just a port", "8082", "", false}, - {"non-existent port", "127.0.0:8080000", "", false}, - - {"too short nodeId", "deadbeef@127.0.0.1:8080", "", false}, - {"too short, not hex nodeId", "this-isnot-hex@127.0.0.1:8080", "", false}, - {"not bech32 nodeId", "xxxm6kmam774klwlh4dhmhaatd7al02m0h0hdap9l@127.0.0.1:8080", "", false}, - - {"too short nodeId w/tcp", "tcp://deadbeef@127.0.0.1:8080", "", false}, - {"too short notHex nodeId w/tcp", "tcp://this-isnot-hex@127.0.0.1:8080", "", false}, - {"not bech32 nodeId w/tcp", "tcp://xxxxm6kmam774klwlh4dhmhaatd7al02m0h0hdap9l@127.0.0.1:8080", "", false}, - {"correct nodeId w/tcp", "tcp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", true}, - - {"no node id", "tcp://@127.0.0.1:8080", "", false}, - {"no node id or IP", "tcp://@", "", false}, - {"tcp no host, w/ port", "tcp://:26656", "", false}, - {"empty", "", "", false}, - {"node id delimiter 1", "@", "", false}, - {"node id delimiter 2", " @", "", false}, - {"node id delimiter 3", " @ ", "", false}, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - addr, err := NewNetAddressFromString(tc.addr) - if tc.correct { - if assert.Nil(t, err, tc.addr) { - assert.Equal(t, tc.expected, addr.String()) - } - } else { - assert.NotNil(t, err, tc.addr) - } - }) - } -} - -func TestNewNetAddressFromStrings(t *testing.T) { - t.Parallel() - - addrs, errs := NewNetAddressFromStrings([]string{ - "127.0.0.1:8080", - "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", - "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.2:8080", - }) - assert.Len(t, errs, 1) - assert.Equal(t, 2, len(addrs)) -} - -func TestNewNetAddressFromIPPort(t *testing.T) { - t.Parallel() - - addr := NewNetAddressFromIPPort("", net.ParseIP("127.0.0.1"), 8080) - assert.Equal(t, "127.0.0.1:8080", addr.String()) -} - -func TestNetAddressProperties(t *testing.T) { - t.Parallel() - - // TODO add more test cases - testCases := []struct { - addr string - valid bool - local bool - routable bool - }{ - {"g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", true, true, false}, - {"g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@ya.ru:80", true, false, true}, - } - - for _, tc := range testCases { - addr, err := NewNetAddressFromString(tc.addr) - require.Nil(t, err) - - err = addr.Validate() - if tc.valid { - assert.NoError(t, err) - } else { - assert.Error(t, err) - } - assert.Equal(t, tc.local, addr.Local()) - assert.Equal(t, tc.routable, addr.Routable()) - } -} - -func TestNetAddressReachabilityTo(t *testing.T) { - t.Parallel() - - // TODO add more test cases - testCases := []struct { - addr string - other string - reachability int - }{ - {"g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8081", 0}, - {"g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@ya.ru:80", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", 1}, - } - - for _, tc := range testCases { - addr, err := NewNetAddressFromString(tc.addr) - require.Nil(t, err) - - other, err := NewNetAddressFromString(tc.other) - require.Nil(t, err) - - assert.Equal(t, tc.reachability, addr.ReachabilityTo(other)) - } -} diff --git a/tm2/pkg/p2p/node_info.go b/tm2/pkg/p2p/node_info.go deleted file mode 100644 index 48ba8f7776b..00000000000 --- a/tm2/pkg/p2p/node_info.go +++ /dev/null @@ -1,156 +0,0 @@ -package p2p - -import ( - "fmt" - - "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore" - "github.com/gnolang/gno/tm2/pkg/strings" - "github.com/gnolang/gno/tm2/pkg/versionset" -) - -const ( - maxNodeInfoSize = 10240 // 10KB - maxNumChannels = 16 // plenty of room for upgrades, for now -) - -// Max size of the NodeInfo struct -func MaxNodeInfoSize() int { - return maxNodeInfoSize -} - -// ------------------------------------------------------------- - -// NodeInfo is the basic node information exchanged -// between two peers during the Tendermint P2P handshake. -type NodeInfo struct { - // Set of protocol versions - VersionSet versionset.VersionSet `json:"version_set"` - - // Authenticate - NetAddress *NetAddress `json:"net_address"` - - // Check compatibility. - // Channels are HexBytes so easier to read as JSON - Network string `json:"network"` // network/chain ID - Software string `json:"software"` // name of immediate software - Version string `json:"version"` // software major.minor.revision - Channels []byte `json:"channels"` // channels this node knows about - - // ASCIIText fields - Moniker string `json:"moniker"` // arbitrary moniker - Other NodeInfoOther `json:"other"` // other application specific data -} - -// NodeInfoOther is the misc. application specific data -type NodeInfoOther struct { - TxIndex string `json:"tx_index"` - RPCAddress string `json:"rpc_address"` -} - -// Validate checks the self-reported NodeInfo is safe. -// It returns an error if there -// are too many Channels, if there are any duplicate Channels, -// if the ListenAddr is malformed, or if the ListenAddr is a host name -// that can not be resolved to some IP. -// TODO: constraints for Moniker/Other? Or is that for the UI ? -// JAE: It needs to be done on the client, but to prevent ambiguous -// unicode characters, maybe it's worth sanitizing it here. -// In the future we might want to validate these, once we have a -// name-resolution system up. -// International clients could then use punycode (or we could use -// url-encoding), and we just need to be careful with how we handle that in our -// clients. (e.g. off by default). -func (info NodeInfo) Validate() error { - // ID is already validated. TODO validate - - // Validate ListenAddr. - if info.NetAddress == nil { - return fmt.Errorf("info.NetAddress cannot be nil") - } - if err := info.NetAddress.ValidateLocal(); err != nil { - return err - } - - // Network is validated in CompatibleWith. - - // Validate Version - if len(info.Version) > 0 && - (!strings.IsASCIIText(info.Version) || strings.ASCIITrim(info.Version) == "") { - return fmt.Errorf("info.Version must be valid ASCII text without tabs, but got %v", info.Version) - } - - // Validate Channels - ensure max and check for duplicates. - if len(info.Channels) > maxNumChannels { - return fmt.Errorf("info.Channels is too long (%v). Max is %v", len(info.Channels), maxNumChannels) - } - channels := make(map[byte]struct{}) - for _, ch := range info.Channels { - _, ok := channels[ch] - if ok { - return fmt.Errorf("info.Channels contains duplicate channel id %v", ch) - } - channels[ch] = struct{}{} - } - - // Validate Moniker. - if !strings.IsASCIIText(info.Moniker) || strings.ASCIITrim(info.Moniker) == "" { - return fmt.Errorf("info.Moniker must be valid non-empty ASCII text without tabs, but got %v", info.Moniker) - } - - // Validate Other. - other := info.Other - txIndex := other.TxIndex - switch txIndex { - case "", eventstore.StatusOn, eventstore.StatusOff: - default: - return fmt.Errorf("info.Other.TxIndex should be either 'on', 'off', or empty string, got '%v'", txIndex) - } - // XXX: Should we be more strict about address formats? - rpcAddr := other.RPCAddress - if len(rpcAddr) > 0 && (!strings.IsASCIIText(rpcAddr) || strings.ASCIITrim(rpcAddr) == "") { - return fmt.Errorf("info.Other.RPCAddress=%v must be valid ASCII text without tabs", rpcAddr) - } - - return nil -} - -func (info NodeInfo) ID() ID { - return info.NetAddress.ID -} - -// CompatibleWith checks if two NodeInfo are compatible with eachother. -// CONTRACT: two nodes are compatible if the Block version and network match -// and they have at least one channel in common. -func (info NodeInfo) CompatibleWith(other NodeInfo) error { - // check protocol versions - _, err := info.VersionSet.CompatibleWith(other.VersionSet) - if err != nil { - return err - } - - // nodes must be on the same network - if info.Network != other.Network { - return fmt.Errorf("Peer is on a different network. Got %v, expected %v", other.Network, info.Network) - } - - // if we have no channels, we're just testing - if len(info.Channels) == 0 { - return nil - } - - // for each of our channels, check if they have it - found := false -OUTER_LOOP: - for _, ch1 := range info.Channels { - for _, ch2 := range other.Channels { - if ch1 == ch2 { - found = true - break OUTER_LOOP // only need one - } - } - } - if !found { - return fmt.Errorf("Peer has no common channels. Our channels: %v ; Peer channels: %v", info.Channels, other.Channels) - } - return nil -} diff --git a/tm2/pkg/p2p/node_info_test.go b/tm2/pkg/p2p/node_info_test.go deleted file mode 100644 index 58f1dab8854..00000000000 --- a/tm2/pkg/p2p/node_info_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package p2p - -import ( - "fmt" - "net" - "testing" - - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/versionset" - "github.com/stretchr/testify/assert" -) - -func TestNodeInfoValidate(t *testing.T) { - t.Parallel() - - // empty fails - ni := NodeInfo{} - assert.Error(t, ni.Validate()) - - channels := make([]byte, maxNumChannels) - for i := 0; i < maxNumChannels; i++ { - channels[i] = byte(i) - } - dupChannels := make([]byte, 5) - copy(dupChannels, channels[:5]) - dupChannels = append(dupChannels, testCh) - - nonAscii := "¢§µ" - emptyTab := fmt.Sprintf("\t") - emptySpace := fmt.Sprintf(" ") - - testCases := []struct { - testName string - malleateNodeInfo func(*NodeInfo) - expectErr bool - }{ - {"Too Many Channels", func(ni *NodeInfo) { ni.Channels = append(channels, byte(maxNumChannels)) }, true}, //nolint: gocritic - {"Duplicate Channel", func(ni *NodeInfo) { ni.Channels = dupChannels }, true}, - {"Good Channels", func(ni *NodeInfo) { ni.Channels = ni.Channels[:5] }, false}, - - {"Nil NetAddress", func(ni *NodeInfo) { ni.NetAddress = nil }, true}, - {"Zero NetAddress ID", func(ni *NodeInfo) { ni.NetAddress.ID = "" }, true}, - {"Invalid NetAddress IP", func(ni *NodeInfo) { ni.NetAddress.IP = net.IP([]byte{0x00}) }, true}, - - {"Non-ASCII Version", func(ni *NodeInfo) { ni.Version = nonAscii }, true}, - {"Empty tab Version", func(ni *NodeInfo) { ni.Version = emptyTab }, true}, - {"Empty space Version", func(ni *NodeInfo) { ni.Version = emptySpace }, true}, - {"Empty Version", func(ni *NodeInfo) { ni.Version = "" }, false}, - - {"Non-ASCII Moniker", func(ni *NodeInfo) { ni.Moniker = nonAscii }, true}, - {"Empty tab Moniker", func(ni *NodeInfo) { ni.Moniker = emptyTab }, true}, - {"Empty space Moniker", func(ni *NodeInfo) { ni.Moniker = emptySpace }, true}, - {"Empty Moniker", func(ni *NodeInfo) { ni.Moniker = "" }, true}, - {"Good Moniker", func(ni *NodeInfo) { ni.Moniker = "hey its me" }, false}, - - {"Non-ASCII TxIndex", func(ni *NodeInfo) { ni.Other.TxIndex = nonAscii }, true}, - {"Empty tab TxIndex", func(ni *NodeInfo) { ni.Other.TxIndex = emptyTab }, true}, - {"Empty space TxIndex", func(ni *NodeInfo) { ni.Other.TxIndex = emptySpace }, true}, - {"Empty TxIndex", func(ni *NodeInfo) { ni.Other.TxIndex = "" }, false}, - {"Off TxIndex", func(ni *NodeInfo) { ni.Other.TxIndex = "off" }, false}, - - {"Non-ASCII RPCAddress", func(ni *NodeInfo) { ni.Other.RPCAddress = nonAscii }, true}, - {"Empty tab RPCAddress", func(ni *NodeInfo) { ni.Other.RPCAddress = emptyTab }, true}, - {"Empty space RPCAddress", func(ni *NodeInfo) { ni.Other.RPCAddress = emptySpace }, true}, - {"Empty RPCAddress", func(ni *NodeInfo) { ni.Other.RPCAddress = "" }, false}, - {"Good RPCAddress", func(ni *NodeInfo) { ni.Other.RPCAddress = "0.0.0.0:26657" }, false}, - } - - nodeKey := NodeKey{PrivKey: ed25519.GenPrivKey()} - name := "testing" - - // test case passes - ni = testNodeInfo(nodeKey.ID(), name) - ni.Channels = channels - assert.NoError(t, ni.Validate()) - - for _, tc := range testCases { - ni := testNodeInfo(nodeKey.ID(), name) - ni.Channels = channels - tc.malleateNodeInfo(&ni) - err := ni.Validate() - if tc.expectErr { - assert.Error(t, err, tc.testName) - } else { - assert.NoError(t, err, tc.testName) - } - } -} - -func TestNodeInfoCompatible(t *testing.T) { - t.Parallel() - - nodeKey1 := NodeKey{PrivKey: ed25519.GenPrivKey()} - nodeKey2 := NodeKey{PrivKey: ed25519.GenPrivKey()} - name := "testing" - - var newTestChannel byte = 0x2 - - // test NodeInfo is compatible - ni1 := testNodeInfo(nodeKey1.ID(), name) - ni2 := testNodeInfo(nodeKey2.ID(), name) - assert.NoError(t, ni1.CompatibleWith(ni2)) - - // add another channel; still compatible - ni2.Channels = []byte{newTestChannel, testCh} - assert.NoError(t, ni1.CompatibleWith(ni2)) - - // wrong NodeInfo type is not compatible - _, netAddr := CreateRoutableAddr() - ni3 := NodeInfo{NetAddress: netAddr} - assert.Error(t, ni1.CompatibleWith(ni3)) - - testCases := []struct { - testName string - malleateNodeInfo func(*NodeInfo) - }{ - {"Bad block version", func(ni *NodeInfo) { - ni.VersionSet.Set(versionset.VersionInfo{Name: "Block", Version: "badversion"}) - }}, - {"Wrong block version", func(ni *NodeInfo) { - ni.VersionSet.Set(versionset.VersionInfo{Name: "Block", Version: "v999.999.999-wrong"}) - }}, - {"Wrong network", func(ni *NodeInfo) { ni.Network += "-wrong" }}, - {"No common channels", func(ni *NodeInfo) { ni.Channels = []byte{newTestChannel} }}, - } - - for i, tc := range testCases { - t.Logf("case #%v", i) - ni := testNodeInfo(nodeKey2.ID(), name) - tc.malleateNodeInfo(&ni) - fmt.Printf("case #%v\n", i) - assert.Error(t, ni1.CompatibleWith(ni)) - } -} diff --git a/tm2/pkg/p2p/peer.go b/tm2/pkg/p2p/peer.go index ef2ddcf2c25..d67f7d1ba8c 100644 --- a/tm2/pkg/p2p/peer.go +++ b/tm2/pkg/p2p/peer.go @@ -6,169 +6,132 @@ import ( "net" "github.com/gnolang/gno/tm2/pkg/cmap" - connm "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/service" ) -// Peer is an interface representing a peer connected on a reactor. -type Peer interface { - service.Service - FlushStop() - - ID() ID // peer's cryptographic ID - RemoteIP() net.IP // remote IP of the connection - RemoteAddr() net.Addr // remote address of the connection - - IsOutbound() bool // did we dial the peer - IsPersistent() bool // do we redial this peer when we disconnect - - CloseConn() error // close original connection - - NodeInfo() NodeInfo // peer's info - Status() connm.ConnectionStatus - SocketAddr() *NetAddress // actual address of the socket - - Send(byte, []byte) bool - TrySend(byte, []byte) bool - - Set(string, interface{}) - Get(string) interface{} +type ConnConfig struct { + MConfig conn.MConnConfig + ReactorsByCh map[byte]Reactor + ChDescs []*conn.ChannelDescriptor + OnPeerError func(Peer, error) } -// ---------------------------------------------------------- - -// peerConn contains the raw connection and its config. -type peerConn struct { - outbound bool - persistent bool - conn net.Conn // source connection - - socketAddr *NetAddress - - // cached RemoteIP() - ip net.IP +// ConnInfo wraps the remote peer connection +type ConnInfo struct { + Outbound bool // flag indicating if the connection is dialed + Persistent bool // flag indicating if the connection is persistent + Private bool // flag indicating if the peer is private (not shared) + Conn net.Conn // the source connection + RemoteIP net.IP // the remote IP of the peer + SocketAddr *types.NetAddress } -func newPeerConn( - outbound, persistent bool, - conn net.Conn, - socketAddr *NetAddress, -) peerConn { - return peerConn{ - outbound: outbound, - persistent: persistent, - conn: conn, - socketAddr: socketAddr, - } -} - -// ID only exists for SecretConnection. -// NOTE: Will panic if conn is not *SecretConnection. -func (pc peerConn) ID() ID { - return (pc.conn.(*connm.SecretConnection).RemotePubKey()).Address().ID() -} - -// Return the IP from the connection RemoteAddr -func (pc peerConn) RemoteIP() net.IP { - if pc.ip != nil { - return pc.ip - } - - host, _, err := net.SplitHostPort(pc.conn.RemoteAddr().String()) - if err != nil { - panic(err) - } - - ips, err := net.LookupIP(host) - if err != nil { - panic(err) - } - - pc.ip = ips[0] - - return pc.ip +type multiplexConn interface { + FlushStop() + Start() error + Stop() error + Send(byte, []byte) bool + TrySend(byte, []byte) bool + SetLogger(*slog.Logger) + Status() conn.ConnectionStatus + String() string } -// peer implements Peer. -// +// peer is a wrapper for a remote peer // Before using a peer, you will need to perform a handshake on connection. type peer struct { service.BaseService - // raw peerConn and the multiplex connection - peerConn - mconn *connm.MConnection - - // peer's node info and the channel it knows about - // channels = nodeInfo.Channels - // cached to avoid copying nodeInfo in hasChannel - nodeInfo NodeInfo - channels []byte + connInfo *ConnInfo // Metadata about the connection + nodeInfo types.NodeInfo // Information about the peer's node + mConn multiplexConn // The multiplexed connection - // User data - Data *cmap.CMap + data *cmap.CMap // Arbitrary data store associated with the peer } -type PeerOption func(*peer) - +// newPeer creates an uninitialized peer instance func newPeer( - pc peerConn, - mConfig connm.MConnConfig, - nodeInfo NodeInfo, - reactorsByCh map[byte]Reactor, - chDescs []*connm.ChannelDescriptor, - onPeerError func(Peer, interface{}), - options ...PeerOption, -) *peer { + connInfo *ConnInfo, + nodeInfo types.NodeInfo, + mConfig *ConnConfig, +) Peer { p := &peer{ - peerConn: pc, + connInfo: connInfo, nodeInfo: nodeInfo, - channels: nodeInfo.Channels, // TODO - Data: cmap.NewCMap(), + data: cmap.NewCMap(), } - p.mconn = createMConnection( - pc.conn, - p, - reactorsByCh, - chDescs, - onPeerError, + p.mConn = p.createMConnection( + connInfo.Conn, mConfig, ) + p.BaseService = *service.NewBaseService(nil, "Peer", p) - for _, option := range options { - option(p) - } return p } -// String representation. +// RemoteIP returns the IP from the remote connection +func (p *peer) RemoteIP() net.IP { + return p.connInfo.RemoteIP +} + +// RemoteAddr returns the address from the remote connection +func (p *peer) RemoteAddr() net.Addr { + return p.connInfo.Conn.RemoteAddr() +} + func (p *peer) String() string { - if p.outbound { - return fmt.Sprintf("Peer{%v %v out}", p.mconn, p.ID()) + if p.connInfo.Outbound { + return fmt.Sprintf("Peer{%s %s out}", p.mConn, p.ID()) } - return fmt.Sprintf("Peer{%v %v in}", p.mconn, p.ID()) + return fmt.Sprintf("Peer{%s %s in}", p.mConn, p.ID()) } -// --------------------------------------------------- -// Implements service.Service +// IsOutbound returns true if the connection is outbound, false otherwise. +func (p *peer) IsOutbound() bool { + return p.connInfo.Outbound +} + +// IsPersistent returns true if the peer is persistent, false otherwise. +func (p *peer) IsPersistent() bool { + return p.connInfo.Persistent +} + +// IsPrivate returns true if the peer is private, false otherwise. +func (p *peer) IsPrivate() bool { + return p.connInfo.Private +} + +// SocketAddr returns the address of the socket. +// For outbound peers, it's the address dialed (after DNS resolution). +// For inbound peers, it's the address returned by the underlying connection +// (not what's reported in the peer's NodeInfo). +func (p *peer) SocketAddr() *types.NetAddress { + return p.connInfo.SocketAddr +} + +// CloseConn closes original connection. +// Used for cleaning up in cases where the peer had not been started at all. +func (p *peer) CloseConn() error { + return p.connInfo.Conn.Close() +} -// SetLogger implements BaseService. func (p *peer) SetLogger(l *slog.Logger) { p.Logger = l - p.mconn.SetLogger(l) + p.mConn.SetLogger(l) } -// OnStart implements BaseService. func (p *peer) OnStart() error { if err := p.BaseService.OnStart(); err != nil { - return err + return fmt.Errorf("unable to start base service, %w", err) } - if err := p.mconn.Start(); err != nil { - return err + if err := p.mConn.Start(); err != nil { + return fmt.Errorf("unable to start multiplex connection, %w", err) } return nil @@ -179,164 +142,105 @@ func (p *peer) OnStart() error { // NOTE: it is not safe to call this method more than once. func (p *peer) FlushStop() { p.BaseService.OnStop() - p.mconn.FlushStop() // stop everything and close the conn + p.mConn.FlushStop() // stop everything and close the conn } // OnStop implements BaseService. func (p *peer) OnStop() { p.BaseService.OnStop() - p.mconn.Stop() // stop everything and close the conn -} -// --------------------------------------------------- -// Implements Peer - -// ID returns the peer's ID - the hex encoded hash of its pubkey. -func (p *peer) ID() ID { - return p.nodeInfo.NetAddress.ID -} - -// IsOutbound returns true if the connection is outbound, false otherwise. -func (p *peer) IsOutbound() bool { - return p.peerConn.outbound + if err := p.mConn.Stop(); err != nil { + p.Logger.Error( + "unable to gracefully close mConn", + "err", + err, + ) + } } -// IsPersistent returns true if the peer is persistent, false otherwise. -func (p *peer) IsPersistent() bool { - return p.peerConn.persistent +// ID returns the peer's ID - the hex encoded hash of its pubkey. +func (p *peer) ID() types.ID { + return p.nodeInfo.PeerID } // NodeInfo returns a copy of the peer's NodeInfo. -func (p *peer) NodeInfo() NodeInfo { +func (p *peer) NodeInfo() types.NodeInfo { return p.nodeInfo } -// SocketAddr returns the address of the socket. -// For outbound peers, it's the address dialed (after DNS resolution). -// For inbound peers, it's the address returned by the underlying connection -// (not what's reported in the peer's NodeInfo). -func (p *peer) SocketAddr() *NetAddress { - return p.peerConn.socketAddr -} - // Status returns the peer's ConnectionStatus. -func (p *peer) Status() connm.ConnectionStatus { - return p.mconn.Status() +func (p *peer) Status() conn.ConnectionStatus { + return p.mConn.Status() } // Send msg bytes to the channel identified by chID byte. Returns false if the // send queue is full after timeout, specified by MConnection. func (p *peer) Send(chID byte, msgBytes []byte) bool { - if !p.IsRunning() { - // see Switch#Broadcast, where we fetch the list of peers and loop over + if !p.IsRunning() || !p.hasChannel(chID) { + // see MultiplexSwitch#Broadcast, where we fetch the list of peers and loop over // them - while we're looping, one peer may be removed and stopped. return false - } else if !p.hasChannel(chID) { - return false } - res := p.mconn.Send(chID, msgBytes) - return res + + return p.mConn.Send(chID, msgBytes) } // TrySend msg bytes to the channel identified by chID byte. Immediately returns // false if the send queue is full. func (p *peer) TrySend(chID byte, msgBytes []byte) bool { - if !p.IsRunning() { - return false - } else if !p.hasChannel(chID) { + if !p.IsRunning() || !p.hasChannel(chID) { return false } - res := p.mconn.TrySend(chID, msgBytes) - return res + + return p.mConn.TrySend(chID, msgBytes) } // Get the data for a given key. -func (p *peer) Get(key string) interface{} { - return p.Data.Get(key) +func (p *peer) Get(key string) any { + return p.data.Get(key) } // Set sets the data for the given key. -func (p *peer) Set(key string, data interface{}) { - p.Data.Set(key, data) +func (p *peer) Set(key string, data any) { + p.data.Set(key, data) } // hasChannel returns true if the peer reported // knowing about the given chID. func (p *peer) hasChannel(chID byte) bool { - for _, ch := range p.channels { + for _, ch := range p.nodeInfo.Channels { if ch == chID { return true } } - // NOTE: probably will want to remove this - // but could be helpful while the feature is new - p.Logger.Debug( - "Unknown channel for peer", - "channel", - chID, - "channels", - p.channels, - ) - return false -} - -// CloseConn closes original connection. Used for cleaning up in cases where the peer had not been started at all. -func (p *peer) CloseConn() error { - return p.peerConn.conn.Close() -} - -// --------------------------------------------------- -// methods only used for testing -// TODO: can we remove these? - -// CloseConn closes the underlying connection -func (pc *peerConn) CloseConn() { - pc.conn.Close() //nolint: errcheck -} -// RemoteAddr returns peer's remote network address. -func (p *peer) RemoteAddr() net.Addr { - return p.peerConn.conn.RemoteAddr() -} - -// CanSend returns true if the send queue is not full, false otherwise. -func (p *peer) CanSend(chID byte) bool { - if !p.IsRunning() { - return false - } - return p.mconn.CanSend(chID) + return false } -// ------------------------------------------------------------------ -// helper funcs - -func createMConnection( - conn net.Conn, - p *peer, - reactorsByCh map[byte]Reactor, - chDescs []*connm.ChannelDescriptor, - onPeerError func(Peer, interface{}), - config connm.MConnConfig, -) *connm.MConnection { +func (p *peer) createMConnection( + c net.Conn, + config *ConnConfig, +) *conn.MConnection { onReceive := func(chID byte, msgBytes []byte) { - reactor := reactorsByCh[chID] + reactor := config.ReactorsByCh[chID] if reactor == nil { // Note that its ok to panic here as it's caught in the connm._recover, // which does onPeerError. panic(fmt.Sprintf("Unknown channel %X", chID)) } + reactor.Receive(chID, p, msgBytes) } - onError := func(r interface{}) { - onPeerError(p, r) + onError := func(r error) { + config.OnPeerError(p, r) } - return connm.NewMConnectionWithConfig( - conn, - chDescs, + return conn.NewMConnectionWithConfig( + c, + config.ChDescs, onReceive, onError, - config, + config.MConfig, ) } diff --git a/tm2/pkg/p2p/peer_set.go b/tm2/pkg/p2p/peer_set.go deleted file mode 100644 index 396ba56da11..00000000000 --- a/tm2/pkg/p2p/peer_set.go +++ /dev/null @@ -1,147 +0,0 @@ -package p2p - -import ( - "net" - "sync" -) - -// IPeerSet has a (immutable) subset of the methods of PeerSet. -type IPeerSet interface { - Has(key ID) bool - HasIP(ip net.IP) bool - Get(key ID) Peer - List() []Peer - Size() int -} - -// ----------------------------------------------------------------------------- - -// PeerSet is a special structure for keeping a table of peers. -// Iteration over the peers is super fast and thread-safe. -type PeerSet struct { - mtx sync.Mutex - lookup map[ID]*peerSetItem - list []Peer -} - -type peerSetItem struct { - peer Peer - index int -} - -// NewPeerSet creates a new peerSet with a list of initial capacity of 256 items. -func NewPeerSet() *PeerSet { - return &PeerSet{ - lookup: make(map[ID]*peerSetItem), - list: make([]Peer, 0, 256), - } -} - -// Add adds the peer to the PeerSet. -// It returns an error carrying the reason, if the peer is already present. -func (ps *PeerSet) Add(peer Peer) error { - ps.mtx.Lock() - defer ps.mtx.Unlock() - - if ps.lookup[peer.ID()] != nil { - return SwitchDuplicatePeerIDError{peer.ID()} - } - - index := len(ps.list) - // Appending is safe even with other goroutines - // iterating over the ps.list slice. - ps.list = append(ps.list, peer) - ps.lookup[peer.ID()] = &peerSetItem{peer, index} - return nil -} - -// Has returns true if the set contains the peer referred to by this -// peerKey, otherwise false. -func (ps *PeerSet) Has(peerKey ID) bool { - ps.mtx.Lock() - _, ok := ps.lookup[peerKey] - ps.mtx.Unlock() - return ok -} - -// HasIP returns true if the set contains the peer referred to by this IP -// address, otherwise false. -func (ps *PeerSet) HasIP(peerIP net.IP) bool { - ps.mtx.Lock() - defer ps.mtx.Unlock() - - return ps.hasIP(peerIP) -} - -// hasIP does not acquire a lock so it can be used in public methods which -// already lock. -func (ps *PeerSet) hasIP(peerIP net.IP) bool { - for _, item := range ps.lookup { - if item.peer.RemoteIP().Equal(peerIP) { - return true - } - } - - return false -} - -// Get looks up a peer by the provided peerKey. Returns nil if peer is not -// found. -func (ps *PeerSet) Get(peerKey ID) Peer { - ps.mtx.Lock() - defer ps.mtx.Unlock() - item, ok := ps.lookup[peerKey] - if ok { - return item.peer - } - return nil -} - -// Remove discards peer by its Key, if the peer was previously memoized. -// Returns true if the peer was removed, and false if it was not found. -// in the set. -func (ps *PeerSet) Remove(peer Peer) bool { - ps.mtx.Lock() - defer ps.mtx.Unlock() - - item := ps.lookup[peer.ID()] - if item == nil { - return false - } - - index := item.index - // Create a new copy of the list but with one less item. - // (we must copy because we'll be mutating the list). - newList := make([]Peer, len(ps.list)-1) - copy(newList, ps.list) - // If it's the last peer, that's an easy special case. - if index == len(ps.list)-1 { - ps.list = newList - delete(ps.lookup, peer.ID()) - return true - } - - // Replace the popped item with the last item in the old list. - lastPeer := ps.list[len(ps.list)-1] - lastPeerKey := lastPeer.ID() - lastPeerItem := ps.lookup[lastPeerKey] - newList[index] = lastPeer - lastPeerItem.index = index - ps.list = newList - delete(ps.lookup, peer.ID()) - return true -} - -// Size returns the number of unique items in the peerSet. -func (ps *PeerSet) Size() int { - ps.mtx.Lock() - defer ps.mtx.Unlock() - return len(ps.list) -} - -// List returns the threadsafe list of peers. -func (ps *PeerSet) List() []Peer { - ps.mtx.Lock() - defer ps.mtx.Unlock() - return ps.list -} diff --git a/tm2/pkg/p2p/peer_set_test.go b/tm2/pkg/p2p/peer_set_test.go deleted file mode 100644 index 7aca84d59b0..00000000000 --- a/tm2/pkg/p2p/peer_set_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package p2p - -import ( - "net" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/service" -) - -// mockPeer for testing the PeerSet -type mockPeer struct { - service.BaseService - ip net.IP - id ID -} - -func (mp *mockPeer) FlushStop() { mp.Stop() } -func (mp *mockPeer) TrySend(chID byte, msgBytes []byte) bool { return true } -func (mp *mockPeer) Send(chID byte, msgBytes []byte) bool { return true } -func (mp *mockPeer) NodeInfo() NodeInfo { return NodeInfo{} } -func (mp *mockPeer) Status() ConnectionStatus { return ConnectionStatus{} } -func (mp *mockPeer) ID() ID { return mp.id } -func (mp *mockPeer) IsOutbound() bool { return false } -func (mp *mockPeer) IsPersistent() bool { return true } -func (mp *mockPeer) Get(s string) interface{} { return s } -func (mp *mockPeer) Set(string, interface{}) {} -func (mp *mockPeer) RemoteIP() net.IP { return mp.ip } -func (mp *mockPeer) SocketAddr() *NetAddress { return nil } -func (mp *mockPeer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: mp.ip, Port: 8800} } -func (mp *mockPeer) CloseConn() error { return nil } - -// Returns a mock peer -func newMockPeer(ip net.IP) *mockPeer { - if ip == nil { - ip = net.IP{127, 0, 0, 1} - } - nodeKey := NodeKey{PrivKey: ed25519.GenPrivKey()} - return &mockPeer{ - ip: ip, - id: nodeKey.ID(), - } -} - -func TestPeerSetAddRemoveOne(t *testing.T) { - t.Parallel() - - peerSet := NewPeerSet() - - var peerList []Peer - for i := 0; i < 5; i++ { - p := newMockPeer(net.IP{127, 0, 0, byte(i)}) - if err := peerSet.Add(p); err != nil { - t.Error(err) - } - peerList = append(peerList, p) - } - - n := len(peerList) - // 1. Test removing from the front - for i, peerAtFront := range peerList { - removed := peerSet.Remove(peerAtFront) - assert.True(t, removed) - wantSize := n - i - 1 - for j := 0; j < 2; j++ { - assert.Equal(t, false, peerSet.Has(peerAtFront.ID()), "#%d Run #%d: failed to remove peer", i, j) - assert.Equal(t, wantSize, peerSet.Size(), "#%d Run #%d: failed to remove peer and decrement size", i, j) - // Test the route of removing the now non-existent element - removed := peerSet.Remove(peerAtFront) - assert.False(t, removed) - } - } - - // 2. Next we are testing removing the peer at the end - // a) Replenish the peerSet - for _, peer := range peerList { - if err := peerSet.Add(peer); err != nil { - t.Error(err) - } - } - - // b) In reverse, remove each element - for i := n - 1; i >= 0; i-- { - peerAtEnd := peerList[i] - removed := peerSet.Remove(peerAtEnd) - assert.True(t, removed) - assert.Equal(t, false, peerSet.Has(peerAtEnd.ID()), "#%d: failed to remove item at end", i) - assert.Equal(t, i, peerSet.Size(), "#%d: differing sizes after peerSet.Remove(atEndPeer)", i) - } -} - -func TestPeerSetAddRemoveMany(t *testing.T) { - t.Parallel() - peerSet := NewPeerSet() - - peers := []Peer{} - N := 100 - for i := 0; i < N; i++ { - peer := newMockPeer(net.IP{127, 0, 0, byte(i)}) - if err := peerSet.Add(peer); err != nil { - t.Errorf("Failed to add new peer") - } - if peerSet.Size() != i+1 { - t.Errorf("Failed to add new peer and increment size") - } - peers = append(peers, peer) - } - - for i, peer := range peers { - removed := peerSet.Remove(peer) - assert.True(t, removed) - if peerSet.Has(peer.ID()) { - t.Errorf("Failed to remove peer") - } - if peerSet.Size() != len(peers)-i-1 { - t.Errorf("Failed to remove peer and decrement size") - } - } -} - -func TestPeerSetAddDuplicate(t *testing.T) { - t.Parallel() - peerSet := NewPeerSet() - peer := newMockPeer(nil) - - n := 20 - errsChan := make(chan error) - // Add the same asynchronously to test the - // concurrent guarantees of our APIs, and - // our expectation in the end is that only - // one addition succeeded, but the rest are - // instances of ErrSwitchDuplicatePeer. - for i := 0; i < n; i++ { - go func() { - errsChan <- peerSet.Add(peer) - }() - } - - // Now collect and tally the results - errsTally := make(map[string]int) - for i := 0; i < n; i++ { - err := <-errsChan - - switch err.(type) { - case SwitchDuplicatePeerIDError: - errsTally["duplicateID"]++ - default: - errsTally["other"]++ - } - } - - // Our next procedure is to ensure that only one addition - // succeeded and that the rest are each ErrSwitchDuplicatePeer. - wantErrCount, gotErrCount := n-1, errsTally["duplicateID"] - assert.Equal(t, wantErrCount, gotErrCount, "invalid ErrSwitchDuplicatePeer count") - - wantNilErrCount, gotNilErrCount := 1, errsTally["other"] - assert.Equal(t, wantNilErrCount, gotNilErrCount, "invalid nil errCount") -} - -func TestPeerSetGet(t *testing.T) { - t.Parallel() - - var ( - peerSet = NewPeerSet() - peer = newMockPeer(nil) - ) - - assert.Nil(t, peerSet.Get(peer.ID()), "expecting a nil lookup, before .Add") - - if err := peerSet.Add(peer); err != nil { - t.Fatalf("Failed to add new peer: %v", err) - } - - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - // Add them asynchronously to test the - // concurrent guarantees of our APIs. - wg.Add(1) - go func(i int) { - defer wg.Done() - have, want := peerSet.Get(peer.ID()), peer - assert.Equal(t, have, want, "%d: have %v, want %v", i, have, want) - }(i) - } - wg.Wait() -} diff --git a/tm2/pkg/p2p/peer_test.go b/tm2/pkg/p2p/peer_test.go index 28217c4486e..a74ea9e96a4 100644 --- a/tm2/pkg/p2p/peer_test.go +++ b/tm2/pkg/p2p/peer_test.go @@ -1,233 +1,630 @@ package p2p import ( + "errors" "fmt" - golog "log" + "io" + "log/slog" "net" "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/cmap" "github.com/gnolang/gno/tm2/pkg/p2p/config" "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/mock" + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/gnolang/gno/tm2/pkg/service" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestPeerBasic(t *testing.T) { +func TestPeer_Properties(t *testing.T) { t.Parallel() - assert, require := assert.New(t), require.New(t) + t.Run("connection info", func(t *testing.T) { + t.Parallel() - // simulate remote peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() + t.Run("remote IP", func(t *testing.T) { + t.Parallel() - p, err := createOutboundPeerAndPerformHandshake(t, rp.Addr(), cfg, conn.DefaultMConnConfig()) - require.Nil(err) + var ( + info = &ConnInfo{ + RemoteIP: net.IP{127, 0, 0, 1}, + } - err = p.Start() - require.Nil(err) - defer p.Stop() + p = &peer{ + connInfo: info, + } + ) - assert.True(p.IsRunning()) - assert.True(p.IsOutbound()) - assert.False(p.IsPersistent()) - p.persistent = true - assert.True(p.IsPersistent()) - assert.Equal(rp.Addr().DialString(), p.RemoteAddr().String()) - assert.Equal(rp.ID(), p.ID()) -} + assert.Equal(t, info.RemoteIP, p.RemoteIP()) + }) -func TestPeerSend(t *testing.T) { - t.Parallel() + t.Run("remote address", func(t *testing.T) { + t.Parallel() - assert, require := assert.New(t), require.New(t) + tcpAddr, err := net.ResolveTCPAddr("tcp", "localhost:8080") + require.NoError(t, err) - config := cfg + var ( + info = &ConnInfo{ + Conn: &mock.Conn{ + RemoteAddrFn: func() net.Addr { + return tcpAddr + }, + }, + } - // simulate remote peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: config} - rp.Start() - defer rp.Stop() + p = &peer{ + connInfo: info, + } + ) - p, err := createOutboundPeerAndPerformHandshake(t, rp.Addr(), config, conn.DefaultMConnConfig()) - require.Nil(err) + assert.Equal(t, tcpAddr.String(), p.RemoteAddr().String()) + }) - err = p.Start() - require.Nil(err) + t.Run("socket address", func(t *testing.T) { + t.Parallel() - defer p.Stop() + tcpAddr, err := net.ResolveTCPAddr("tcp", "localhost:8080") + require.NoError(t, err) - assert.True(p.CanSend(testCh)) - assert.True(p.Send(testCh, []byte("Asylum"))) -} + netAddr, err := types.NewNetAddress(types.GenerateNodeKey().ID(), tcpAddr) + require.NoError(t, err) -func createOutboundPeerAndPerformHandshake( - t *testing.T, - addr *NetAddress, - config *config.P2PConfig, - mConfig conn.MConnConfig, -) (*peer, error) { - t.Helper() - - chDescs := []*conn.ChannelDescriptor{ - {ID: testCh, Priority: 1}, - } - reactorsByCh := map[byte]Reactor{testCh: NewTestReactor(chDescs, true)} - pk := ed25519.GenPrivKey() - pc, err := testOutboundPeerConn(addr, config, false, pk) - if err != nil { - return nil, err - } - timeout := 1 * time.Second - ourNodeInfo := testNodeInfo(addr.ID, "host_peer") - peerNodeInfo, err := handshake(pc.conn, timeout, ourNodeInfo) - if err != nil { - return nil, err - } - - p := newPeer(pc, mConfig, peerNodeInfo, reactorsByCh, chDescs, func(p Peer, r interface{}) {}) - p.SetLogger(log.NewTestingLogger(t).With("peer", addr)) - return p, nil -} + var ( + info = &ConnInfo{ + SocketAddr: netAddr, + } + + p = &peer{ + connInfo: info, + } + ) + + assert.Equal(t, netAddr.String(), p.SocketAddr().String()) + }) + + t.Run("set logger", func(t *testing.T) { + t.Parallel() + + var ( + l = slog.New(slog.NewTextHandler(io.Discard, nil)) + + p = &peer{ + mConn: &mock.MConn{}, + } + ) + + p.SetLogger(l) + + assert.Equal(t, l, p.Logger) + }) + + t.Run("peer start", func(t *testing.T) { + t.Parallel() + + var ( + expectedErr = errors.New("some error") + + mConn = &mock.MConn{ + StartFn: func() error { + return expectedErr + }, + } + + p = &peer{ + mConn: mConn, + } + ) + + assert.ErrorIs(t, p.OnStart(), expectedErr) + }) + + t.Run("peer stop", func(t *testing.T) { + t.Parallel() + + var ( + stopCalled = false + expectedErr = errors.New("some error") + + mConn = &mock.MConn{ + StopFn: func() error { + stopCalled = true + + return expectedErr + }, + } + + p = &peer{ + mConn: mConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + p.OnStop() + + assert.True(t, stopCalled) + }) + + t.Run("flush stop", func(t *testing.T) { + t.Parallel() + + var ( + stopCalled = false + + mConn = &mock.MConn{ + FlushFn: func() { + stopCalled = true + }, + } + + p = &peer{ + mConn: mConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + p.FlushStop() + + assert.True(t, stopCalled) + }) -func testDial(addr *NetAddress, cfg *config.P2PConfig) (net.Conn, error) { - if cfg.TestDialFail { - return nil, fmt.Errorf("dial err (peerConfig.DialFail == true)") - } + t.Run("node info fetch", func(t *testing.T) { + t.Parallel() - conn, err := addr.DialTimeout(cfg.DialTimeout) - if err != nil { - return nil, err - } - return conn, nil + var ( + info = types.NodeInfo{ + Network: "gnoland", + } + + p = &peer{ + nodeInfo: info, + } + ) + + assert.Equal(t, info, p.NodeInfo()) + }) + + t.Run("node status fetch", func(t *testing.T) { + t.Parallel() + + var ( + status = conn.ConnectionStatus{ + Duration: 5 * time.Second, + } + + mConn = &mock.MConn{ + StatusFn: func() conn.ConnectionStatus { + return status + }, + } + + p = &peer{ + mConn: mConn, + } + ) + + assert.Equal(t, status, p.Status()) + }) + + t.Run("string representation", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + outbound bool + }{ + { + "outbound", + true, + }, + { + "inbound", + false, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + id = types.GenerateNodeKey().ID() + mConnStr = "description" + + p = &peer{ + mConn: &mock.MConn{ + StringFn: func() string { + return mConnStr + }, + }, + nodeInfo: types.NodeInfo{ + PeerID: id, + }, + connInfo: &ConnInfo{ + Outbound: testCase.outbound, + }, + } + + direction = "in" + ) + + if testCase.outbound { + direction = "out" + } + + assert.Contains( + t, + p.String(), + fmt.Sprintf( + "Peer{%s %s %s}", + mConnStr, + id, + direction, + ), + ) + }) + } + }) + + t.Run("outbound information", func(t *testing.T) { + t.Parallel() + + p := &peer{ + connInfo: &ConnInfo{ + Outbound: true, + }, + } + + assert.True( + t, + p.IsOutbound(), + ) + }) + + t.Run("persistent information", func(t *testing.T) { + t.Parallel() + + p := &peer{ + connInfo: &ConnInfo{ + Persistent: true, + }, + } + + assert.True(t, p.IsPersistent()) + }) + + t.Run("initial conn close", func(t *testing.T) { + t.Parallel() + + var ( + closeErr = errors.New("close error") + + mockConn = &mock.Conn{ + CloseFn: func() error { + return closeErr + }, + } + + p = &peer{ + connInfo: &ConnInfo{ + Conn: mockConn, + }, + } + ) + + assert.ErrorIs(t, p.CloseConn(), closeErr) + }) + }) } -func testOutboundPeerConn( - addr *NetAddress, - config *config.P2PConfig, - persistent bool, - ourNodePrivKey crypto.PrivKey, -) (peerConn, error) { - var pc peerConn - conn, err := testDial(addr, config) - if err != nil { - return pc, errors.Wrap(err, "Error creating peer") - } - - pc, err = testPeerConn(conn, config, true, persistent, ourNodePrivKey, addr) - if err != nil { - if cerr := conn.Close(); cerr != nil { - return pc, errors.Wrap(err, cerr.Error()) - } - return pc, err - } +func TestPeer_GetSet(t *testing.T) { + t.Parallel() - // ensure dialed ID matches connection ID - if addr.ID != pc.ID() { - if cerr := conn.Close(); cerr != nil { - return pc, errors.Wrap(err, cerr.Error()) + var ( + key = "key" + data = []byte("random") + + p = &peer{ + data: cmap.NewCMap(), } - return pc, SwitchAuthenticationFailureError{addr, pc.ID()} - } + ) - return pc, nil -} + assert.Nil(t, p.Get(key)) -type remotePeer struct { - PrivKey crypto.PrivKey - Config *config.P2PConfig - addr *NetAddress - channels []byte - listenAddr string - listener net.Listener -} + // Set the key + p.Set(key, data) -func (rp *remotePeer) Addr() *NetAddress { - return rp.addr + assert.Equal(t, data, p.Get(key)) } -func (rp *remotePeer) ID() ID { - return rp.PrivKey.PubKey().Address().ID() -} +func TestPeer_Send(t *testing.T) { + t.Parallel() -func (rp *remotePeer) Start() { - if rp.listenAddr == "" { - rp.listenAddr = "127.0.0.1:0" - } - - l, e := net.Listen("tcp", rp.listenAddr) // any available address - if e != nil { - golog.Fatalf("net.Listen tcp :0: %+v", e) - } - rp.listener = l - rp.addr = NewNetAddress(rp.PrivKey.PubKey().Address().ID(), l.Addr()) - if rp.channels == nil { - rp.channels = []byte{testCh} - } - go rp.accept() -} + t.Run("peer not running", func(t *testing.T) { + t.Parallel() -func (rp *remotePeer) Stop() { - rp.listener.Close() -} + var ( + chID = byte(10) + data = []byte("random") + + capturedSendID byte + capturedSendData []byte + + mockConn = &mock.MConn{ + SendFn: func(c byte, d []byte) bool { + capturedSendID = c + capturedSendData = d + + return true + }, + } + + p = &peer{ + nodeInfo: types.NodeInfo{ + Channels: []byte{ + chID, + }, + }, + mConn: mockConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + // Make sure the send fails + require.False(t, p.Send(chID, data)) + + assert.Empty(t, capturedSendID) + assert.Nil(t, capturedSendData) + }) + + t.Run("peer doesn't have channel", func(t *testing.T) { + t.Parallel() + + var ( + chID = byte(10) + data = []byte("random") + + capturedSendID byte + capturedSendData []byte + + mockConn = &mock.MConn{ + SendFn: func(c byte, d []byte) bool { + capturedSendID = c + capturedSendData = d + + return true + }, + } + + p = &peer{ + nodeInfo: types.NodeInfo{ + Channels: []byte{}, + }, + mConn: mockConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + // Start the peer "multiplexing" + require.NoError(t, p.Start()) + t.Cleanup(func() { + require.NoError(t, p.Stop()) + }) + + // Make sure the send fails + require.False(t, p.Send(chID, data)) + + assert.Empty(t, capturedSendID) + assert.Nil(t, capturedSendData) + }) -func (rp *remotePeer) Dial(addr *NetAddress) (net.Conn, error) { - conn, err := addr.DialTimeout(1 * time.Second) - if err != nil { - return nil, err - } - pc, err := testInboundPeerConn(conn, rp.Config, rp.PrivKey) - if err != nil { - return nil, err - } - _, err = handshake(pc.conn, time.Second, rp.nodeInfo()) - if err != nil { - return nil, err - } - return conn, err + t.Run("valid peer data send", func(t *testing.T) { + t.Parallel() + + var ( + chID = byte(10) + data = []byte("random") + + capturedSendID byte + capturedSendData []byte + + mockConn = &mock.MConn{ + SendFn: func(c byte, d []byte) bool { + capturedSendID = c + capturedSendData = d + + return true + }, + } + + p = &peer{ + nodeInfo: types.NodeInfo{ + Channels: []byte{ + chID, + }, + }, + mConn: mockConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + // Start the peer "multiplexing" + require.NoError(t, p.Start()) + t.Cleanup(func() { + require.NoError(t, p.Stop()) + }) + + // Make sure the send is valid + require.True(t, p.Send(chID, data)) + + assert.Equal(t, chID, capturedSendID) + assert.Equal(t, data, capturedSendData) + }) } -func (rp *remotePeer) accept() { - conns := []net.Conn{} +func TestPeer_TrySend(t *testing.T) { + t.Parallel() + + t.Run("peer not running", func(t *testing.T) { + t.Parallel() + + var ( + chID = byte(10) + data = []byte("random") - for { - conn, err := rp.listener.Accept() - if err != nil { - golog.Printf("Failed to accept conn: %+v", err) - for _, conn := range conns { - _ = conn.Close() + capturedSendID byte + capturedSendData []byte + + mockConn = &mock.MConn{ + TrySendFn: func(c byte, d []byte) bool { + capturedSendID = c + capturedSendData = d + + return true + }, } - return - } - pc, err := testInboundPeerConn(conn, rp.Config, rp.PrivKey) - if err != nil { - golog.Fatalf("Failed to create a peer: %+v", err) - } + p = &peer{ + nodeInfo: types.NodeInfo{ + Channels: []byte{ + chID, + }, + }, + mConn: mockConn, + } + ) - _, err = handshake(pc.conn, time.Second, rp.nodeInfo()) - if err != nil { - golog.Fatalf("Failed to perform handshake: %+v", err) - } + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + // Make sure the send fails + require.False(t, p.TrySend(chID, data)) + + assert.Empty(t, capturedSendID) + assert.Nil(t, capturedSendData) + }) + + t.Run("peer doesn't have channel", func(t *testing.T) { + t.Parallel() + + var ( + chID = byte(10) + data = []byte("random") + + capturedSendID byte + capturedSendData []byte + + mockConn = &mock.MConn{ + TrySendFn: func(c byte, d []byte) bool { + capturedSendID = c + capturedSendData = d + + return true + }, + } + + p = &peer{ + nodeInfo: types.NodeInfo{ + Channels: []byte{}, + }, + mConn: mockConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + // Start the peer "multiplexing" + require.NoError(t, p.Start()) + t.Cleanup(func() { + require.NoError(t, p.Stop()) + }) + + // Make sure the send fails + require.False(t, p.TrySend(chID, data)) + + assert.Empty(t, capturedSendID) + assert.Nil(t, capturedSendData) + }) + + t.Run("valid peer data send", func(t *testing.T) { + t.Parallel() + + var ( + chID = byte(10) + data = []byte("random") - conns = append(conns, conn) - } + capturedSendID byte + capturedSendData []byte + + mockConn = &mock.MConn{ + TrySendFn: func(c byte, d []byte) bool { + capturedSendID = c + capturedSendData = d + + return true + }, + } + + p = &peer{ + nodeInfo: types.NodeInfo{ + Channels: []byte{ + chID, + }, + }, + mConn: mockConn, + } + ) + + p.BaseService = *service.NewBaseService(nil, "Peer", p) + + // Start the peer "multiplexing" + require.NoError(t, p.Start()) + t.Cleanup(func() { + require.NoError(t, p.Stop()) + }) + + // Make sure the send is valid + require.True(t, p.TrySend(chID, data)) + + assert.Equal(t, chID, capturedSendID) + assert.Equal(t, data, capturedSendData) + }) } -func (rp *remotePeer) nodeInfo() NodeInfo { - return NodeInfo{ - VersionSet: testVersionSet(), - NetAddress: rp.Addr(), - Network: "testing", - Version: "1.2.3-rc0-deadbeef", - Channels: rp.channels, - Moniker: "remote_peer", - } +func TestPeer_NewPeer(t *testing.T) { + t.Parallel() + + tcpAddr, err := net.ResolveTCPAddr("tcp", "localhost:8080") + require.NoError(t, err) + + netAddr, err := types.NewNetAddress(types.GenerateNodeKey().ID(), tcpAddr) + require.NoError(t, err) + + var ( + connInfo = &ConnInfo{ + Outbound: false, + Persistent: true, + Conn: &mock.Conn{}, + RemoteIP: tcpAddr.IP, + SocketAddr: netAddr, + } + + mConfig = &ConnConfig{ + MConfig: conn.MConfigFromP2P(config.DefaultP2PConfig()), + ReactorsByCh: make(map[byte]Reactor), + ChDescs: make([]*conn.ChannelDescriptor, 0), + OnPeerError: nil, + } + ) + + assert.NotPanics(t, func() { + _ = newPeer(connInfo, types.NodeInfo{}, mConfig) + }) } diff --git a/tm2/pkg/p2p/set.go b/tm2/pkg/p2p/set.go new file mode 100644 index 00000000000..9905fe0ac66 --- /dev/null +++ b/tm2/pkg/p2p/set.go @@ -0,0 +1,121 @@ +package p2p + +import ( + "sync" + + "github.com/gnolang/gno/tm2/pkg/p2p/types" +) + +type set struct { + mux sync.RWMutex + + peers map[types.ID]Peer + outbound uint64 + inbound uint64 +} + +// newSet creates an empty peer set +func newSet() *set { + return &set{ + peers: make(map[types.ID]Peer), + outbound: 0, + inbound: 0, + } +} + +// Add adds the peer to the set +func (s *set) Add(peer Peer) { + s.mux.Lock() + defer s.mux.Unlock() + + s.peers[peer.ID()] = peer + + if peer.IsOutbound() { + s.outbound += 1 + + return + } + + s.inbound += 1 +} + +// Has returns true if the set contains the peer referred to by this +// peerKey, otherwise false. +func (s *set) Has(peerKey types.ID) bool { + s.mux.RLock() + defer s.mux.RUnlock() + + _, exists := s.peers[peerKey] + + return exists +} + +// Get looks up a peer by the peer ID. Returns nil if peer is not +// found. +func (s *set) Get(key types.ID) Peer { + s.mux.RLock() + defer s.mux.RUnlock() + + p, found := s.peers[key] + if !found { + // TODO change this to an error, it doesn't make + // sense to propagate an implementation detail like this + return nil + } + + return p.(Peer) +} + +// Remove discards peer by its Key, if the peer was previously memoized. +// Returns true if the peer was removed, and false if it was not found. +// in the set. +func (s *set) Remove(key types.ID) bool { + s.mux.Lock() + defer s.mux.Unlock() + + p, found := s.peers[key] + if !found { + return false + } + + delete(s.peers, key) + + if p.(Peer).IsOutbound() { + s.outbound -= 1 + + return true + } + + s.inbound -= 1 + + return true +} + +// NumInbound returns the number of inbound peers +func (s *set) NumInbound() uint64 { + s.mux.RLock() + defer s.mux.RUnlock() + + return s.inbound +} + +// NumOutbound returns the number of outbound peers +func (s *set) NumOutbound() uint64 { + s.mux.RLock() + defer s.mux.RUnlock() + + return s.outbound +} + +// List returns the list of peers +func (s *set) List() []Peer { + s.mux.RLock() + defer s.mux.RUnlock() + + peers := make([]Peer, 0) + for _, p := range s.peers { + peers = append(peers, p.(Peer)) + } + + return peers +} diff --git a/tm2/pkg/p2p/set_test.go b/tm2/pkg/p2p/set_test.go new file mode 100644 index 00000000000..ced35538a9b --- /dev/null +++ b/tm2/pkg/p2p/set_test.go @@ -0,0 +1,146 @@ +package p2p + +import ( + "sort" + "testing" + + "github.com/gnolang/gno/tm2/pkg/p2p/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSet_Add(t *testing.T) { + t.Parallel() + + var ( + numPeers = 100 + peers = mock.GeneratePeers(t, numPeers) + + s = newSet() + ) + + for _, peer := range peers { + // Add the peer + s.Add(peer) + + // Make sure the peer is present + assert.True(t, s.Has(peer.ID())) + } + + assert.EqualValues(t, numPeers, s.NumInbound()+s.NumOutbound()) +} + +func TestSet_Remove(t *testing.T) { + t.Parallel() + + var ( + numPeers = 100 + peers = mock.GeneratePeers(t, numPeers) + + s = newSet() + ) + + // Add the initial peers + for _, peer := range peers { + // Add the peer + s.Add(peer) + + // Make sure the peer is present + require.True(t, s.Has(peer.ID())) + } + + require.EqualValues(t, numPeers, s.NumInbound()+s.NumOutbound()) + + // Remove the peers + // Add the initial peers + for _, peer := range peers { + // Add the peer + s.Remove(peer.ID()) + + // Make sure the peer is present + assert.False(t, s.Has(peer.ID())) + } +} + +func TestSet_Get(t *testing.T) { + t.Parallel() + + t.Run("existing peer", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 100) + s = newSet() + ) + + for _, peer := range peers { + id := peer.ID() + s.Add(peer) + + assert.True(t, s.Get(id).ID() == id) + } + }) + + t.Run("missing peer", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 100) + s = newSet() + ) + + for _, peer := range peers { + s.Add(peer) + } + + p := s.Get("random ID") + assert.Nil(t, p) + }) +} + +func TestSet_List(t *testing.T) { + t.Parallel() + + t.Run("empty peer set", func(t *testing.T) { + t.Parallel() + + // Empty set + s := newSet() + + // Linearize the set + assert.Len(t, s.List(), 0) + }) + + t.Run("existing peer set", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 100) + s = newSet() + ) + + for _, peer := range peers { + s.Add(peer) + } + + // Linearize the set + listedPeers := s.List() + + require.Len(t, listedPeers, len(peers)) + + // Make sure the lists are sorted + // for easier comparison + sort.Slice(listedPeers, func(i, j int) bool { + return listedPeers[i].ID() < listedPeers[j].ID() + }) + + sort.Slice(peers, func(i, j int) bool { + return peers[i].ID() < peers[j].ID() + }) + + // Compare the lists + for index, listedPeer := range listedPeers { + assert.Equal(t, listedPeer.ID(), peers[index].ID()) + } + }) +} diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index b2de68e1ae3..dbc26a2304d 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -2,226 +2,164 @@ package p2p import ( "context" + "crypto/rand" "fmt" "math" + "math/big" "sync" "time" - "github.com/gnolang/gno/tm2/pkg/cmap" - "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/p2p/config" "github.com/gnolang/gno/tm2/pkg/p2p/conn" - "github.com/gnolang/gno/tm2/pkg/random" + "github.com/gnolang/gno/tm2/pkg/p2p/dial" + "github.com/gnolang/gno/tm2/pkg/p2p/events" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/service" "github.com/gnolang/gno/tm2/pkg/telemetry" "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" ) -const ( - // wait a random amount of time from this interval - // before dialing peers or reconnecting to help prevent DoS - dialRandomizerIntervalMilliseconds = 3000 +// defaultDialTimeout is the default wait time for a dial to succeed +var defaultDialTimeout = 3 * time.Second - // repeatedly try to reconnect for a few minutes - // ie. 5 * 20 = 100s - reconnectAttempts = 20 - reconnectInterval = 5 * time.Second +type reactorPeerBehavior struct { + chDescs []*conn.ChannelDescriptor + reactorsByCh map[byte]Reactor - // then move into exponential backoff mode for ~1day - // ie. 3**10 = 16hrs - reconnectBackOffAttempts = 10 - reconnectBackOffBaseSeconds = 3 -) + handlePeerErrFn func(Peer, error) + isPersistentPeerFn func(types.ID) bool + isPrivatePeerFn func(types.ID) bool +} + +func (r *reactorPeerBehavior) ReactorChDescriptors() []*conn.ChannelDescriptor { + return r.chDescs +} + +func (r *reactorPeerBehavior) Reactors() map[byte]Reactor { + return r.reactorsByCh +} -// MConnConfig returns an MConnConfig with fields updated -// from the P2PConfig. -func MConnConfig(cfg *config.P2PConfig) conn.MConnConfig { - mConfig := conn.DefaultMConnConfig() - mConfig.FlushThrottle = cfg.FlushThrottleTimeout - mConfig.SendRate = cfg.SendRate - mConfig.RecvRate = cfg.RecvRate - mConfig.MaxPacketMsgPayloadSize = cfg.MaxPacketMsgPayloadSize - return mConfig +func (r *reactorPeerBehavior) HandlePeerError(p Peer, err error) { + r.handlePeerErrFn(p, err) } -// PeerFilterFunc to be implemented by filter hooks after a new Peer has been -// fully setup. -type PeerFilterFunc func(IPeerSet, Peer) error +func (r *reactorPeerBehavior) IsPersistentPeer(id types.ID) bool { + return r.isPersistentPeerFn(id) +} -// ----------------------------------------------------------------------------- +func (r *reactorPeerBehavior) IsPrivatePeer(id types.ID) bool { + return r.isPrivatePeerFn(id) +} -// Switch handles peer connections and exposes an API to receive incoming messages +// MultiplexSwitch handles peer connections and exposes an API to receive incoming messages // on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one // or more `Channels`. So while sending outgoing messages is typically performed on the peer, // incoming messages are received on the reactor. -type Switch struct { +type MultiplexSwitch struct { service.BaseService - config *config.P2PConfig - reactors map[string]Reactor - chDescs []*conn.ChannelDescriptor - reactorsByCh map[byte]Reactor - peers *PeerSet - dialing *cmap.CMap - reconnecting *cmap.CMap - nodeInfo NodeInfo // our node info - nodeKey *NodeKey // our node privkey - // peers addresses with whom we'll maintain constant connection - persistentPeersAddrs []*NetAddress + ctx context.Context + cancelFn context.CancelFunc - transport Transport + maxInboundPeers uint64 + maxOutboundPeers uint64 - filterTimeout time.Duration - peerFilters []PeerFilterFunc + reactors map[string]Reactor + peerBehavior *reactorPeerBehavior - rng *random.Rand // seed for randomizing dial times and orders -} + peers PeerSet // currently active peer set (live connections) + persistentPeers sync.Map // ID -> *NetAddress; peers whose connections are constant + privatePeers sync.Map // ID -> nothing; lookup table of peers who are not shared + transport Transport -// NetAddress returns the address the switch is listening on. -func (sw *Switch) NetAddress() *NetAddress { - addr := sw.transport.NetAddress() - return &addr + dialQueue *dial.Queue + events *events.Events } -// SwitchOption sets an optional parameter on the Switch. -type SwitchOption func(*Switch) - -// NewSwitch creates a new Switch with the given config. -func NewSwitch( - cfg *config.P2PConfig, +// NewMultiplexSwitch creates a new MultiplexSwitch with the given config. +func NewMultiplexSwitch( transport Transport, - options ...SwitchOption, -) *Switch { - sw := &Switch{ - config: cfg, - reactors: make(map[string]Reactor), - chDescs: make([]*conn.ChannelDescriptor, 0), - reactorsByCh: make(map[byte]Reactor), - peers: NewPeerSet(), - dialing: cmap.NewCMap(), - reconnecting: cmap.NewCMap(), - transport: transport, - filterTimeout: defaultFilterTimeout, - persistentPeersAddrs: make([]*NetAddress, 0), - } - - // Ensure we have a completely undeterministic PRNG. - sw.rng = random.NewRand() - - sw.BaseService = *service.NewBaseService(nil, "P2P Switch", sw) - - for _, option := range options { - option(sw) - } - - return sw -} + opts ...SwitchOption, +) *MultiplexSwitch { + defaultCfg := config.DefaultP2PConfig() -// SwitchFilterTimeout sets the timeout used for peer filters. -func SwitchFilterTimeout(timeout time.Duration) SwitchOption { - return func(sw *Switch) { sw.filterTimeout = timeout } -} - -// SwitchPeerFilters sets the filters for rejection of new peers. -func SwitchPeerFilters(filters ...PeerFilterFunc) SwitchOption { - return func(sw *Switch) { sw.peerFilters = filters } -} - -// --------------------------------------------------------------------- -// Switch setup - -// AddReactor adds the given reactor to the switch. -// NOTE: Not goroutine safe. -func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { - for _, chDesc := range reactor.GetChannels() { - chID := chDesc.ID - // No two reactors can share the same channel. - if sw.reactorsByCh[chID] != nil { - panic(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) - } - sw.chDescs = append(sw.chDescs, chDesc) - sw.reactorsByCh[chID] = reactor + sw := &MultiplexSwitch{ + reactors: make(map[string]Reactor), + peers: newSet(), + transport: transport, + dialQueue: dial.NewQueue(), + events: events.New(), + maxInboundPeers: defaultCfg.MaxNumInboundPeers, + maxOutboundPeers: defaultCfg.MaxNumOutboundPeers, } - sw.reactors[name] = reactor - reactor.SetSwitch(sw) - return reactor -} -// RemoveReactor removes the given Reactor from the Switch. -// NOTE: Not goroutine safe. -func (sw *Switch) RemoveReactor(name string, reactor Reactor) { - for _, chDesc := range reactor.GetChannels() { - // remove channel description - for i := 0; i < len(sw.chDescs); i++ { - if chDesc.ID == sw.chDescs[i].ID { - sw.chDescs = append(sw.chDescs[:i], sw.chDescs[i+1:]...) - break - } - } - delete(sw.reactorsByCh, chDesc.ID) + // Set up the peer dial behavior + sw.peerBehavior = &reactorPeerBehavior{ + chDescs: make([]*conn.ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + handlePeerErrFn: sw.StopPeerForError, + isPersistentPeerFn: func(id types.ID) bool { + return sw.isPersistentPeer(id) + }, + isPrivatePeerFn: func(id types.ID) bool { + return sw.isPrivatePeer(id) + }, } - delete(sw.reactors, name) - reactor.SetSwitch(nil) -} -// Reactors returns a map of reactors registered on the switch. -// NOTE: Not goroutine safe. -func (sw *Switch) Reactors() map[string]Reactor { - return sw.reactors -} + sw.BaseService = *service.NewBaseService(nil, "P2P MultiplexSwitch", sw) -// Reactor returns the reactor with the given name. -// NOTE: Not goroutine safe. -func (sw *Switch) Reactor(name string) Reactor { - return sw.reactors[name] -} + // Set up the context + sw.ctx, sw.cancelFn = context.WithCancel(context.Background()) -// SetNodeInfo sets the switch's NodeInfo for checking compatibility and handshaking with other nodes. -// NOTE: Not goroutine safe. -func (sw *Switch) SetNodeInfo(nodeInfo NodeInfo) { - sw.nodeInfo = nodeInfo -} + // Apply the options + for _, opt := range opts { + opt(sw) + } -// NodeInfo returns the switch's NodeInfo. -// NOTE: Not goroutine safe. -func (sw *Switch) NodeInfo() NodeInfo { - return sw.nodeInfo + return sw } -// SetNodeKey sets the switch's private key for authenticated encryption. -// NOTE: Not goroutine safe. -func (sw *Switch) SetNodeKey(nodeKey *NodeKey) { - sw.nodeKey = nodeKey +// Subscribe registers to live events happening on the p2p Switch. +// Returns the notification channel, along with an unsubscribe method +func (sw *MultiplexSwitch) Subscribe(filterFn events.EventFilter) (<-chan events.Event, func()) { + return sw.events.Subscribe(filterFn) } // --------------------------------------------------------------------- // Service start/stop // OnStart implements BaseService. It starts all the reactors and peers. -func (sw *Switch) OnStart() error { +func (sw *MultiplexSwitch) OnStart() error { // Start reactors for _, reactor := range sw.reactors { - err := reactor.Start() - if err != nil { - return errors.Wrap(err, "failed to start %v", reactor) + if err := reactor.Start(); err != nil { + return fmt.Errorf("unable to start reactor %w", err) } } - // Start accepting Peers. - go sw.acceptRoutine() + // Run the peer accept routine. + // The accept routine asynchronously accepts + // and processes incoming peer connections + go sw.runAcceptLoop(sw.ctx) + + // Run the dial routine. + // The dial routine parses items in the dial queue + // and initiates outbound peer connections + go sw.runDialLoop(sw.ctx) + + // Run the redial routine. + // The redial routine monitors for important + // peer disconnects, and attempts to reconnect + // to them + go sw.runRedialLoop(sw.ctx) return nil } // OnStop implements BaseService. It stops all peers and reactors. -func (sw *Switch) OnStop() { - // Stop transport - if t, ok := sw.transport.(TransportLifecycle); ok { - err := t.Close() - if err != nil { - sw.Logger.Error("Error stopping transport on stop: ", "error", err) - } - } +func (sw *MultiplexSwitch) OnStop() { + // Close all hanging threads + sw.cancelFn() // Stop peers for _, p := range sw.peers.List() { @@ -229,465 +167,504 @@ func (sw *Switch) OnStop() { } // Stop reactors - sw.Logger.Debug("Switch: Stopping reactors") for _, reactor := range sw.reactors { - reactor.Stop() - } -} - -// --------------------------------------------------------------------- -// Peers - -// Broadcast runs a go routine for each attempted send, which will block trying -// to send for defaultSendTimeoutSeconds. Returns a channel which receives -// success values for each attempted send (false if times out). Channel will be -// closed once msg bytes are sent to all peers (or time out). -// -// NOTE: Broadcast uses goroutines, so order of broadcast may not be preserved. -func (sw *Switch) Broadcast(chID byte, msgBytes []byte) chan bool { - startTime := time.Now() - - sw.Logger.Debug( - "Broadcast", - "channel", chID, - "value", fmt.Sprintf("%X", msgBytes), - ) - - peers := sw.peers.List() - var wg sync.WaitGroup - wg.Add(len(peers)) - successChan := make(chan bool, len(peers)) - - for _, peer := range peers { - go func(p Peer) { - defer wg.Done() - success := p.Send(chID, msgBytes) - successChan <- success - }(peer) - } - - go func() { - wg.Wait() - close(successChan) - if telemetry.MetricsEnabled() { - metrics.BroadcastTxTimer.Record(context.Background(), time.Since(startTime).Milliseconds()) - } - }() - - return successChan -} - -// NumPeers returns the count of outbound/inbound and outbound-dialing peers. -func (sw *Switch) NumPeers() (outbound, inbound, dialing int) { - peers := sw.peers.List() - for _, peer := range peers { - if peer.IsOutbound() { - outbound++ - } else { - inbound++ + if err := reactor.Stop(); err != nil { + sw.Logger.Error("unable to gracefully stop reactor", "err", err) } } - dialing = sw.dialing.Size() - return } -// MaxNumOutboundPeers returns a maximum number of outbound peers. -func (sw *Switch) MaxNumOutboundPeers() int { - return sw.config.MaxNumOutboundPeers +// Broadcast broadcasts the given data to the given channel, +// across the entire switch peer set, without blocking +func (sw *MultiplexSwitch) Broadcast(chID byte, data []byte) { + for _, p := range sw.peers.List() { + go func() { + // This send context is managed internally + // by the Peer's underlying connection implementation + if !p.Send(chID, data) { + sw.Logger.Error( + "unable to perform broadcast", + "chID", chID, + "peerID", p.ID(), + ) + } + }() + } } // Peers returns the set of peers that are connected to the switch. -func (sw *Switch) Peers() IPeerSet { +func (sw *MultiplexSwitch) Peers() PeerSet { return sw.peers } // StopPeerForError disconnects from a peer due to external error. -// If the peer is persistent, it will attempt to reconnect. -// TODO: make record depending on reason. -func (sw *Switch) StopPeerForError(peer Peer, reason interface{}) { - sw.Logger.Error("Stopping peer for error", "peer", peer, "err", reason) - sw.stopAndRemovePeer(peer, reason) - - if peer.IsPersistent() { - var addr *NetAddress - if peer.IsOutbound() { // socket address for outbound peers - addr = peer.SocketAddr() - } else { // self-reported address for inbound peers - addr = peer.NodeInfo().NetAddress - } - go sw.reconnectToPeer(addr) +// If the peer is persistent, it will attempt to reconnect +func (sw *MultiplexSwitch) StopPeerForError(peer Peer, err error) { + sw.Logger.Error("Stopping peer for error", "peer", peer, "err", err) + + sw.stopAndRemovePeer(peer, err) + + if !peer.IsPersistent() { + // Peer is not a persistent peer, + // no need to initiate a redial + return } -} -// StopPeerGracefully disconnects from a peer gracefully. -// TODO: handle graceful disconnects. -func (sw *Switch) StopPeerGracefully(peer Peer) { - sw.Logger.Info("Stopping peer gracefully") - sw.stopAndRemovePeer(peer, nil) + // Add the peer to the dial queue + sw.DialPeers(peer.SocketAddr()) } -func (sw *Switch) stopAndRemovePeer(peer Peer, reason interface{}) { - sw.transport.Cleanup(peer) - peer.Stop() +func (sw *MultiplexSwitch) stopAndRemovePeer(peer Peer, err error) { + // Remove the peer from the transport + sw.transport.Remove(peer) + + // Close the (original) peer connection + if closeErr := peer.CloseConn(); closeErr != nil { + sw.Logger.Error( + "unable to gracefully close peer connection", + "peer", peer, + "err", closeErr, + ) + } + + // Stop the peer connection multiplexing + if stopErr := peer.Stop(); stopErr != nil { + sw.Logger.Error( + "unable to gracefully stop peer", + "peer", peer, + "err", stopErr, + ) + } + // Alert the reactors of a peer removal for _, reactor := range sw.reactors { - reactor.RemovePeer(peer, reason) + reactor.RemovePeer(peer, err) } // Removing a peer should go last to avoid a situation where a peer // reconnect to our node and the switch calls InitPeer before // RemovePeer is finished. // https://github.com/tendermint/classic/issues/3338 - sw.peers.Remove(peer) + sw.peers.Remove(peer.ID()) + + sw.events.Notify(events.PeerDisconnectedEvent{ + Address: peer.RemoteAddr(), + PeerID: peer.ID(), + Reason: err, + }) } -// reconnectToPeer tries to reconnect to the addr, first repeatedly -// with a fixed interval, then with exponential backoff. -// If no success after all that, it stops trying. -// NOTE: this will keep trying even if the handshake or auth fails. -// TODO: be more explicit with error types so we only retry on certain failures -// - ie. if we're getting ErrDuplicatePeer we can stop -func (sw *Switch) reconnectToPeer(addr *NetAddress) { - if sw.reconnecting.Has(addr.ID.String()) { - return - } - sw.reconnecting.Set(addr.ID.String(), addr) - defer sw.reconnecting.Delete(addr.ID.String()) +// --------------------------------------------------------------------- +// Dialing - start := time.Now() - sw.Logger.Info("Reconnecting to peer", "addr", addr) - for i := 0; i < reconnectAttempts; i++ { - if !sw.IsRunning() { - return - } +func (sw *MultiplexSwitch) runDialLoop(ctx context.Context) { + for { + select { + case <-ctx.Done(): + sw.Logger.Debug("dial context canceled") - err := sw.DialPeerWithAddress(addr) - if err == nil { - return // success - } else if _, ok := err.(CurrentlyDialingOrExistingAddressError); ok { return - } + default: + // Grab a dial item + item := sw.dialQueue.Peek() + if item == nil { + // Nothing to dial + continue + } - sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "addr", addr) - // sleep a set amount - sw.randomSleep(reconnectInterval) - continue - } + // Check if the dial time is right + // for the item + if time.Now().Before(item.Time) { + // Nothing to dial + continue + } - sw.Logger.Error("Failed to reconnect to peer. Beginning exponential backoff", - "addr", addr, "elapsed", time.Since(start)) - for i := 0; i < reconnectBackOffAttempts; i++ { - if !sw.IsRunning() { - return - } + // Pop the item from the dial queue + item = sw.dialQueue.Pop() - // sleep an exponentially increasing amount - sleepIntervalSeconds := math.Pow(reconnectBackOffBaseSeconds, float64(i)) - sw.randomSleep(time.Duration(sleepIntervalSeconds) * time.Second) + // Dial the peer + sw.Logger.Info( + "dialing peer", + "address", item.Address.String(), + ) - err := sw.DialPeerWithAddress(addr) - if err == nil { - return // success - } else if _, ok := err.(CurrentlyDialingOrExistingAddressError); ok { - return - } - sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "addr", addr) - } - sw.Logger.Error("Failed to reconnect to peer. Giving up", "addr", addr, "elapsed", time.Since(start)) -} + peerAddr := item.Address -// --------------------------------------------------------------------- -// Dialing + // Check if the peer is already connected + ps := sw.Peers() + if ps.Has(peerAddr.ID) { + sw.Logger.Warn( + "ignoring dial request for existing peer", + "id", peerAddr.ID, + ) -// DialPeersAsync dials a list of peers asynchronously in random order. -// Used to dial peers from config on startup or from unsafe-RPC (trusted sources). -// It ignores NetAddressLookupError. However, if there are other errors, first -// encounter is returned. -// Nop if there are no peers. -func (sw *Switch) DialPeersAsync(peers []string) error { - netAddrs, errs := NewNetAddressFromStrings(peers) - // report all the errors - for _, err := range errs { - sw.Logger.Error("Error in peer's address", "err", err) - } - // return first non-NetAddressLookupError error - for _, err := range errs { - if _, ok := err.(NetAddressLookupError); ok { - continue - } - return err - } - sw.dialPeersAsync(netAddrs) - return nil -} + continue + } -func (sw *Switch) dialPeersAsync(netAddrs []*NetAddress) { - ourAddr := sw.NetAddress() + // Create a dial context + dialCtx, cancelFn := context.WithTimeout(ctx, defaultDialTimeout) + defer cancelFn() - // permute the list, dial them in random order. - perm := sw.rng.Perm(len(netAddrs)) - for i := 0; i < len(perm); i++ { - go func(i int) { - j := perm[i] - addr := netAddrs[j] + p, err := sw.transport.Dial(dialCtx, *peerAddr, sw.peerBehavior) + if err != nil { + sw.Logger.Error( + "unable to dial peer", + "peer", peerAddr, + "err", err, + ) - if addr.Same(ourAddr) { - sw.Logger.Debug("Ignore attempt to connect to ourselves", "addr", addr, "ourAddr", ourAddr) - return + continue } - sw.randomSleep(0) + // Register the peer with the switch + if err = sw.addPeer(p); err != nil { + sw.Logger.Error( + "unable to add peer", + "peer", p, + "err", err, + ) - err := sw.DialPeerWithAddress(addr) - if err != nil { - switch err.(type) { - case SwitchConnectToSelfError, SwitchDuplicatePeerIDError, CurrentlyDialingOrExistingAddressError: - sw.Logger.Debug("Error dialing peer", "err", err) - default: - sw.Logger.Error("Error dialing peer", "err", err) + sw.transport.Remove(p) + + if !p.IsRunning() { + continue + } + + if stopErr := p.Stop(); stopErr != nil { + sw.Logger.Error( + "unable to gracefully stop peer", + "peer", p, + "err", stopErr, + ) } } - }(i) + + // Log the telemetry + sw.logTelemetry() + } } } -// DialPeerWithAddress dials the given peer and runs sw.addPeer if it connects -// and authenticates successfully. -// If we're currently dialing this address or it belongs to an existing peer, -// CurrentlyDialingOrExistingAddressError is returned. -func (sw *Switch) DialPeerWithAddress(addr *NetAddress) error { - if sw.IsDialingOrExistingAddress(addr) { - return CurrentlyDialingOrExistingAddressError{addr.String()} +// runRedialLoop starts the persistent peer redial loop +func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + + type backoffItem struct { + lastDialTime time.Time + attempts int } - sw.dialing.Set(addr.ID.String(), addr) - defer sw.dialing.Delete(addr.ID.String()) + var ( + backoffMap = make(map[types.ID]*backoffItem) - return sw.addOutboundPeerWithConfig(addr, sw.config) -} + mux sync.RWMutex + ) -// sleep for interval plus some random amount of ms on [0, dialRandomizerIntervalMilliseconds] -func (sw *Switch) randomSleep(interval time.Duration) { - r := time.Duration(sw.rng.Int63n(dialRandomizerIntervalMilliseconds)) * time.Millisecond - time.Sleep(r + interval) -} + setBackoffItem := func(id types.ID, item *backoffItem) { + mux.Lock() + defer mux.Unlock() -// IsDialingOrExistingAddress returns true if switch has a peer with the given -// address or dialing it at the moment. -func (sw *Switch) IsDialingOrExistingAddress(addr *NetAddress) bool { - return sw.dialing.Has(addr.ID.String()) || - sw.peers.Has(addr.ID) || - (!sw.config.AllowDuplicateIP && sw.peers.HasIP(addr.IP)) -} + backoffMap[id] = item + } -// AddPersistentPeers allows you to set persistent peers. It ignores -// NetAddressLookupError. However, if there are other errors, first encounter is -// returned. -func (sw *Switch) AddPersistentPeers(addrs []string) error { - sw.Logger.Info("Adding persistent peers", "addrs", addrs) - netAddrs, errs := NewNetAddressFromStrings(addrs) - // report all the errors - for _, err := range errs { - sw.Logger.Error("Error in peer's address", "err", err) - } - // return first non-NetAddressLookupError error - for _, err := range errs { - if _, ok := err.(NetAddressLookupError); ok { - continue - } - return err + getBackoffItem := func(id types.ID) *backoffItem { + mux.RLock() + defer mux.RUnlock() + + return backoffMap[id] } - sw.persistentPeersAddrs = netAddrs - return nil -} -func (sw *Switch) isPeerPersistentFn() func(*NetAddress) bool { - return func(na *NetAddress) bool { - for _, pa := range sw.persistentPeersAddrs { - if pa.Equals(na) { + clearBackoffItem := func(id types.ID) { + mux.Lock() + defer mux.Unlock() + + delete(backoffMap, id) + } + + subCh, unsubFn := sw.Subscribe(func(event events.Event) bool { + if event.Type() != events.PeerConnected { + return false + } + + ev := event.(events.PeerConnectedEvent) + + return sw.isPersistentPeer(ev.PeerID) + }) + defer unsubFn() + + // redialFn goes through the persistent peer list + // and dials missing peers + redialFn := func() { + var ( + peers = sw.Peers() + peersToDial = make([]*types.NetAddress, 0) + ) + + sw.persistentPeers.Range(func(key, value any) bool { + var ( + id = key.(types.ID) + addr = value.(*types.NetAddress) + ) + + // Check if the peer is part of the peer set + // or is scheduled for dialing + if peers.Has(id) || sw.dialQueue.Has(addr) { return true } - } - return false - } -} -func (sw *Switch) acceptRoutine() { - for { - p, err := sw.transport.Accept(peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - reactorsByCh: sw.reactorsByCh, - isPersistent: sw.isPeerPersistentFn(), + peersToDial = append(peersToDial, addr) + + return true }) - if err != nil { - switch err := err.(type) { - case RejectedError: - if err.IsSelf() { - // TODO: warn? - } - sw.Logger.Info( - "Inbound Peer rejected", - "err", err, - "numPeers", sw.peers.Size(), - ) + if len(peersToDial) == 0 { + // No persistent peers are missing + return + } - continue - case FilterTimeoutError: - sw.Logger.Error( - "Peer filter timed out", - "err", err, - ) + // Calculate the dial items + dialItems := make([]dial.Item, 0, len(peersToDial)) + for _, p := range peersToDial { + item := getBackoffItem(p.ID) + if item == nil { + dialItem := dial.Item{ + Time: time.Now(), + Address: p, + } + + dialItems = append(dialItems, dialItem) + setBackoffItem(p.ID, &backoffItem{dialItem.Time, 0}) continue - case TransportClosedError: - sw.Logger.Error( - "Stopped accept routine, as transport is closed", - "numPeers", sw.peers.Size(), - ) - default: - sw.Logger.Error( - "Accept on transport errored", - "err", err, - "numPeers", sw.peers.Size(), - ) - // We could instead have a retry loop around the acceptRoutine, - // but that would need to stop and let the node shutdown eventually. - // So might as well panic and let process managers restart the node. - // There's no point in letting the node run without the acceptRoutine, - // since it won't be able to accept new connections. - panic(fmt.Errorf("accept routine exited: %w", err)) } - break + setBackoffItem(p.ID, &backoffItem{ + lastDialTime: time.Now().Add( + calculateBackoff( + item.attempts, + time.Second, + 10*time.Minute, + ), + ), + attempts: item.attempts + 1, + }) } - // Ignore connection if we already have enough peers. - _, in, _ := sw.NumPeers() - if in >= sw.config.MaxNumInboundPeers { - sw.Logger.Info( - "Ignoring inbound connection: already have enough inbound peers", - "address", p.SocketAddr(), - "have", in, - "max", sw.config.MaxNumInboundPeers, - ) + // Add the peers to the dial queue + sw.dialItems(dialItems...) + } + + // Run the initial redial loop on start, + // in case persistent peer connections are not + // active + redialFn() + + for { + select { + case <-ctx.Done(): + sw.Logger.Debug("redial crawl context canceled") + + return + case <-ticker.C: + redialFn() + case event := <-subCh: + // A persistent peer reconnected, + // clear their redial queue + ev := event.(events.PeerConnectedEvent) + + clearBackoffItem(ev.PeerID) + } + } +} + +// calculateBackoff calculates a backoff time, +// based on the number of attempts and range limits +func calculateBackoff( + attempts int, + minTimeout time.Duration, + maxTimeout time.Duration, +) time.Duration { + var ( + minTime = time.Second * 1 + maxTime = time.Second * 60 + multiplier = float64(2) // exponential + ) + + // Check the min limit + if minTimeout > 0 { + minTime = minTimeout + } + + // Check the max limit + if maxTimeout > 0 { + maxTime = maxTimeout + } + + // Sanity check the range + if minTime >= maxTime { + return maxTime + } + + // Calculate the backoff duration + var ( + base = float64(minTime) + calculated = base * math.Pow(multiplier, float64(attempts)) + ) + + // Attempt to calculate the jitter factor + n, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err == nil { + jitterFactor := float64(n.Int64()) / float64(math.MaxInt64) // range [0, 1] + + calculated = jitterFactor*(calculated-base) + base + } - sw.transport.Cleanup(p) + // Prevent overflow for int64 (duration) cast + if calculated > float64(math.MaxInt64) { + return maxTime + } + + duration := time.Duration(calculated) + + // Clamp the duration within bounds + if duration < minTime { + return minTime + } + if duration > maxTime { + return maxTime + } + + return duration +} + +// DialPeers adds the peers to the dial queue for async dialing. +// To monitor dial progress, subscribe to adequate p2p MultiplexSwitch events +func (sw *MultiplexSwitch) DialPeers(peerAddrs ...*types.NetAddress) { + for _, peerAddr := range peerAddrs { + // Check if this is our address + if peerAddr.Same(sw.transport.NetAddress()) { continue } - if err := sw.addPeer(p); err != nil { - sw.transport.Cleanup(p) - if p.IsRunning() { - _ = p.Stop() - } - sw.Logger.Info( - "Ignoring inbound connection: error while adding peer", - "err", err, - "id", p.ID(), + // Ignore dial if the limit is reached + if out := sw.Peers().NumOutbound(); out >= sw.maxOutboundPeers { + sw.Logger.Warn( + "ignoring dial request: already have max outbound peers", + "have", out, + "max", sw.maxOutboundPeers, ) + + continue } + + item := dial.Item{ + Time: time.Now(), + Address: peerAddr, + } + + sw.dialQueue.Push(item) } } -// dial the peer; make secret connection; authenticate against the dialed ID; -// add the peer. -// if dialing fails, start the reconnect loop. If handshake fails, it's over. -// If peer is started successfully, reconnectLoop will start when -// StopPeerForError is called. -func (sw *Switch) addOutboundPeerWithConfig( - addr *NetAddress, - cfg *config.P2PConfig, -) error { - sw.Logger.Info("Dialing peer", "address", addr) - - // XXX(xla): Remove the leakage of test concerns in implementation. - if cfg.TestDialFail { - go sw.reconnectToPeer(addr) - return fmt.Errorf("dial err (peerConfig.DialFail == true)") - } - - p, err := sw.transport.Dial(*addr, peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - isPersistent: sw.isPeerPersistentFn(), - reactorsByCh: sw.reactorsByCh, - }) - if err != nil { - if e, ok := err.(RejectedError); ok { - if e.IsSelf() { - // TODO: warn? - return err - } +// dialItems adds custom dial items for the multiplex switch +func (sw *MultiplexSwitch) dialItems(dialItems ...dial.Item) { + for _, dialItem := range dialItems { + // Check if this is our address + if dialItem.Address.Same(sw.transport.NetAddress()) { + continue } - // retry persistent peers after - // any dial error besides IsSelf() - if sw.isPeerPersistentFn()(addr) { - go sw.reconnectToPeer(addr) + // Ignore dial if the limit is reached + if out := sw.Peers().NumOutbound(); out >= sw.maxOutboundPeers { + sw.Logger.Warn( + "ignoring dial request: already have max outbound peers", + "have", out, + "max", sw.maxOutboundPeers, + ) + + continue } - return err + sw.dialQueue.Push(dialItem) } +} - if err := sw.addPeer(p); err != nil { - sw.transport.Cleanup(p) - if p.IsRunning() { - _ = p.Stop() - } - return err - } +// isPersistentPeer returns a flag indicating if a peer +// is present in the persistent peer set +func (sw *MultiplexSwitch) isPersistentPeer(id types.ID) bool { + _, persistent := sw.persistentPeers.Load(id) - return nil + return persistent } -func (sw *Switch) filterPeer(p Peer) error { - // Avoid duplicate - if sw.peers.Has(p.ID()) { - return RejectedError{id: p.ID(), isDuplicate: true} - } - - errc := make(chan error, len(sw.peerFilters)) +// isPrivatePeer returns a flag indicating if a peer +// is present in the private peer set +func (sw *MultiplexSwitch) isPrivatePeer(id types.ID) bool { + _, persistent := sw.privatePeers.Load(id) - for _, f := range sw.peerFilters { - go func(f PeerFilterFunc, p Peer, errc chan<- error) { - errc <- f(sw.peers, p) - }(f, p, errc) - } + return persistent +} - for i := 0; i < cap(errc); i++ { +// runAcceptLoop is the main powerhouse method +// for accepting incoming peer connections, filtering them, +// and persisting them +func (sw *MultiplexSwitch) runAcceptLoop(ctx context.Context) { + for { select { - case err := <-errc: + case <-ctx.Done(): + sw.Logger.Debug("switch context close received") + + return + default: + p, err := sw.transport.Accept(ctx, sw.peerBehavior) if err != nil { - return RejectedError{id: p.ID(), err: err, isFiltered: true} + sw.Logger.Error( + "error encountered during peer connection accept", + "err", err, + ) + + continue + } + + // Ignore connection if we already have enough peers. + if in := sw.Peers().NumInbound(); in >= sw.maxInboundPeers { + sw.Logger.Info( + "Ignoring inbound connection: already have enough inbound peers", + "address", p.SocketAddr(), + "have", in, + "max", sw.maxInboundPeers, + ) + + sw.transport.Remove(p) + + continue + } + + // There are open peer slots, add peers + if err := sw.addPeer(p); err != nil { + sw.transport.Remove(p) + + if p.IsRunning() { + _ = p.Stop() + } + + sw.Logger.Info( + "Ignoring inbound connection: error while adding peer", + "err", err, + "id", p.ID(), + ) } - case <-time.After(sw.filterTimeout): - return FilterTimeoutError{} } } - - return nil } -// addPeer starts up the Peer and adds it to the Switch. Error is returned if +// addPeer starts up the Peer and adds it to the MultiplexSwitch. Error is returned if // the peer is filtered out or failed to start or can't be added. -func (sw *Switch) addPeer(p Peer) error { - if err := sw.filterPeer(p); err != nil { - return err - } - +func (sw *MultiplexSwitch) addPeer(p Peer) error { p.SetLogger(sw.Logger.With("peer", p.SocketAddr())) - // Handle the shut down case where the switch has stopped but we're - // concurrently trying to add a peer. - if !sw.IsRunning() { - // XXX should this return an error or just log and terminate? - sw.Logger.Error("Won't start a peer - switch is not running", "peer", p) - return nil - } - // Add some data to the peer, which is required by reactors. for _, reactor := range sw.reactors { p = reactor.InitPeer(p) @@ -696,19 +673,15 @@ func (sw *Switch) addPeer(p Peer) error { // Start the peer's send/recv routines. // Must start it before adding it to the peer set // to prevent Start and Stop from being called concurrently. - err := p.Start() - if err != nil { - // Should never happen + if err := p.Start(); err != nil { sw.Logger.Error("Error starting peer", "err", err, "peer", p) + return err } - // Add the peer to PeerSet. Do this before starting the reactors + // Add the peer to the peer set. Do this before starting the reactors // so that if Receive errors, we will find the peer and remove it. - // Add should not err since we already checked peers.Has(). - if err := sw.peers.Add(p); err != nil { - return err - } + sw.peers.Add(p) // Start all the reactor protocols on the peer. for _, reactor := range sw.reactors { @@ -717,29 +690,28 @@ func (sw *Switch) addPeer(p Peer) error { sw.Logger.Info("Added peer", "peer", p) - // Update the telemetry data - sw.logTelemetry() + sw.events.Notify(events.PeerConnectedEvent{ + Address: p.RemoteAddr(), + PeerID: p.ID(), + }) return nil } // logTelemetry logs the switch telemetry data // to global metrics funnels -func (sw *Switch) logTelemetry() { +func (sw *MultiplexSwitch) logTelemetry() { // Update the telemetry data if !telemetry.MetricsEnabled() { return } // Fetch the number of peers - outbound, inbound, dialing := sw.NumPeers() + outbound, inbound := sw.peers.NumOutbound(), sw.peers.NumInbound() // Log the outbound peer count metrics.OutboundPeers.Record(context.Background(), int64(outbound)) // Log the inbound peer count metrics.InboundPeers.Record(context.Background(), int64(inbound)) - - // Log the dialing peer count - metrics.DialingPeers.Record(context.Background(), int64(dialing)) } diff --git a/tm2/pkg/p2p/switch_option.go b/tm2/pkg/p2p/switch_option.go new file mode 100644 index 00000000000..83a6920f2cd --- /dev/null +++ b/tm2/pkg/p2p/switch_option.go @@ -0,0 +1,61 @@ +package p2p + +import ( + "github.com/gnolang/gno/tm2/pkg/p2p/types" +) + +// SwitchOption is a callback used for configuring the p2p MultiplexSwitch +type SwitchOption func(*MultiplexSwitch) + +// WithReactor sets the p2p switch reactors +func WithReactor(name string, reactor Reactor) SwitchOption { + return func(sw *MultiplexSwitch) { + for _, chDesc := range reactor.GetChannels() { + chID := chDesc.ID + + // No two reactors can share the same channel + if sw.peerBehavior.reactorsByCh[chID] != nil { + continue + } + + sw.peerBehavior.chDescs = append(sw.peerBehavior.chDescs, chDesc) + sw.peerBehavior.reactorsByCh[chID] = reactor + } + + sw.reactors[name] = reactor + + reactor.SetSwitch(sw) + } +} + +// WithPersistentPeers sets the p2p switch's persistent peer set +func WithPersistentPeers(peerAddrs []*types.NetAddress) SwitchOption { + return func(sw *MultiplexSwitch) { + for _, addr := range peerAddrs { + sw.persistentPeers.Store(addr.ID, addr) + } + } +} + +// WithPrivatePeers sets the p2p switch's private peer set +func WithPrivatePeers(peerIDs []types.ID) SwitchOption { + return func(sw *MultiplexSwitch) { + for _, id := range peerIDs { + sw.privatePeers.Store(id, struct{}{}) + } + } +} + +// WithMaxInboundPeers sets the p2p switch's maximum inbound peer limit +func WithMaxInboundPeers(maxInbound uint64) SwitchOption { + return func(sw *MultiplexSwitch) { + sw.maxInboundPeers = maxInbound + } +} + +// WithMaxOutboundPeers sets the p2p switch's maximum outbound peer limit +func WithMaxOutboundPeers(maxOutbound uint64) SwitchOption { + return func(sw *MultiplexSwitch) { + sw.maxOutboundPeers = maxOutbound + } +} diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index a7033b466fe..cd2f793c190 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -1,704 +1,825 @@ package p2p import ( - "bytes" - "errors" - "fmt" - "io" + "context" "net" "sync" - "sync/atomic" "testing" "time" + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/p2p/dial" + "github.com/gnolang/gno/tm2/pkg/p2p/mock" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p/config" - "github.com/gnolang/gno/tm2/pkg/p2p/conn" - "github.com/gnolang/gno/tm2/pkg/testutils" ) -var cfg *config.P2PConfig +func TestMultiplexSwitch_Options(t *testing.T) { + t.Parallel() -func init() { - cfg = config.DefaultP2PConfig() - cfg.PexReactor = true - cfg.AllowDuplicateIP = true -} + t.Run("custom reactors", func(t *testing.T) { + t.Parallel() -type PeerMessage struct { - PeerID ID - Bytes []byte - Counter int -} + var ( + name = "custom reactor" + mockReactor = &mockReactor{ + setSwitchFn: func(s Switch) { + require.NotNil(t, s) + }, + } + ) -type TestReactor struct { - BaseReactor + sw := NewMultiplexSwitch(nil, WithReactor(name, mockReactor)) - mtx sync.Mutex - channels []*conn.ChannelDescriptor - logMessages bool - msgsCounter int - msgsReceived map[byte][]PeerMessage -} + assert.Equal(t, mockReactor, sw.reactors[name]) + }) -func NewTestReactor(channels []*conn.ChannelDescriptor, logMessages bool) *TestReactor { - tr := &TestReactor{ - channels: channels, - logMessages: logMessages, - msgsReceived: make(map[byte][]PeerMessage), - } - tr.BaseReactor = *NewBaseReactor("TestReactor", tr) - tr.SetLogger(log.NewNoopLogger()) - return tr -} + t.Run("persistent peers", func(t *testing.T) { + t.Parallel() -func (tr *TestReactor) GetChannels() []*conn.ChannelDescriptor { - return tr.channels -} + peers := generateNetAddr(t, 10) -func (tr *TestReactor) AddPeer(peer Peer) {} + sw := NewMultiplexSwitch(nil, WithPersistentPeers(peers)) -func (tr *TestReactor) RemovePeer(peer Peer, reason interface{}) {} + for _, p := range peers { + assert.True(t, sw.isPersistentPeer(p.ID)) + } + }) -func (tr *TestReactor) Receive(chID byte, peer Peer, msgBytes []byte) { - if tr.logMessages { - tr.mtx.Lock() - defer tr.mtx.Unlock() - // fmt.Printf("Received: %X, %X\n", chID, msgBytes) - tr.msgsReceived[chID] = append(tr.msgsReceived[chID], PeerMessage{peer.ID(), msgBytes, tr.msgsCounter}) - tr.msgsCounter++ - } -} + t.Run("private peers", func(t *testing.T) { + t.Parallel() -func (tr *TestReactor) getMsgs(chID byte) []PeerMessage { - tr.mtx.Lock() - defer tr.mtx.Unlock() - return tr.msgsReceived[chID] -} + var ( + peers = generateNetAddr(t, 10) + ids = make([]types.ID, 0, len(peers)) + ) -// ----------------------------------------------------------------------------- + for _, p := range peers { + ids = append(ids, p.ID) + } -// convenience method for creating two switches connected to each other. -// XXX: note this uses net.Pipe and not a proper TCP conn -func MakeSwitchPair(_ testing.TB, initSwitch func(int, *Switch) *Switch) (*Switch, *Switch) { - // Create two switches that will be interconnected. - switches := MakeConnectedSwitches(cfg, 2, initSwitch, Connect2Switches) - return switches[0], switches[1] -} + sw := NewMultiplexSwitch(nil, WithPrivatePeers(ids)) -func initSwitchFunc(i int, sw *Switch) *Switch { - // Make two reactors of two channels each - sw.AddReactor("foo", NewTestReactor([]*conn.ChannelDescriptor{ - {ID: byte(0x00), Priority: 10}, - {ID: byte(0x01), Priority: 10}, - }, true)) - sw.AddReactor("bar", NewTestReactor([]*conn.ChannelDescriptor{ - {ID: byte(0x02), Priority: 10}, - {ID: byte(0x03), Priority: 10}, - }, true)) - - return sw -} + for _, p := range peers { + assert.True(t, sw.isPrivatePeer(p.ID)) + } + }) -func TestSwitches(t *testing.T) { - t.Parallel() + t.Run("max inbound peers", func(t *testing.T) { + t.Parallel() - s1, s2 := MakeSwitchPair(t, initSwitchFunc) - defer s1.Stop() - defer s2.Stop() + maxInbound := uint64(500) - if s1.Peers().Size() != 1 { - t.Errorf("Expected exactly 1 peer in s1, got %v", s1.Peers().Size()) - } - if s2.Peers().Size() != 1 { - t.Errorf("Expected exactly 1 peer in s2, got %v", s2.Peers().Size()) - } + sw := NewMultiplexSwitch(nil, WithMaxInboundPeers(maxInbound)) - // Lets send some messages - ch0Msg := []byte("channel zero") - ch1Msg := []byte("channel foo") - ch2Msg := []byte("channel bar") + assert.Equal(t, maxInbound, sw.maxInboundPeers) + }) + + t.Run("max outbound peers", func(t *testing.T) { + t.Parallel() - s1.Broadcast(byte(0x00), ch0Msg) - s1.Broadcast(byte(0x01), ch1Msg) - s1.Broadcast(byte(0x02), ch2Msg) + maxOutbound := uint64(500) - assertMsgReceivedWithTimeout(t, ch0Msg, byte(0x00), s2.Reactor("foo").(*TestReactor), 10*time.Millisecond, 5*time.Second) - assertMsgReceivedWithTimeout(t, ch1Msg, byte(0x01), s2.Reactor("foo").(*TestReactor), 10*time.Millisecond, 5*time.Second) - assertMsgReceivedWithTimeout(t, ch2Msg, byte(0x02), s2.Reactor("bar").(*TestReactor), 10*time.Millisecond, 5*time.Second) + sw := NewMultiplexSwitch(nil, WithMaxOutboundPeers(maxOutbound)) + + assert.Equal(t, maxOutbound, sw.maxOutboundPeers) + }) } -func assertMsgReceivedWithTimeout(t *testing.T, msgBytes []byte, channel byte, reactor *TestReactor, checkPeriod, timeout time.Duration) { - t.Helper() +func TestMultiplexSwitch_Broadcast(t *testing.T) { + t.Parallel() - ticker := time.NewTicker(checkPeriod) - for { - select { - case <-ticker.C: - msgs := reactor.getMsgs(channel) - if len(msgs) > 0 { - if !bytes.Equal(msgs[0].Bytes, msgBytes) { - t.Fatalf("Unexpected message bytes. Wanted: %X, Got: %X", msgBytes, msgs[0].Bytes) - } - return - } + var ( + wg sync.WaitGroup + + expectedChID = byte(10) + expectedData = []byte("broadcast data") - case <-time.After(timeout): - t.Fatalf("Expected to have received 1 message in channel #%v, got zero", channel) + mockTransport = &mockTransport{ + acceptFn: func(_ context.Context, _ PeerBehavior) (Peer, error) { + return nil, errors.New("constant error") + }, } - } -} -func TestSwitchFiltersOutItself(t *testing.T) { - t.Parallel() + peers = mock.GeneratePeers(t, 10) + sw = NewMultiplexSwitch(mockTransport) + ) - s1 := MakeSwitch(cfg, 1, "127.0.0.1", "123.123.123", initSwitchFunc) + require.NoError(t, sw.OnStart()) + t.Cleanup(sw.OnStop) - // simulate s1 having a public IP by creating a remote peer with the same ID - rp := &remotePeer{PrivKey: s1.nodeKey.PrivKey, Config: cfg} - rp.Start() + // Create a new peer set + sw.peers = newSet() - // addr should be rejected in addPeer based on the same ID - err := s1.DialPeerWithAddress(rp.Addr()) - if assert.Error(t, err) { - if err, ok := err.(RejectedError); ok { - if !err.IsSelf() { - t.Errorf("expected self to be rejected") - } - } else { - t.Errorf("expected RejectedError") + for _, p := range peers { + wg.Add(1) + + p.SendFn = func(chID byte, data []byte) bool { + wg.Done() + + require.Equal(t, expectedChID, chID) + assert.Equal(t, expectedData, data) + + return false } + + // Load it up with peers + sw.peers.Add(p) } - rp.Stop() + // Broadcast the data + sw.Broadcast(expectedChID, expectedData) - assertNoPeersAfterTimeout(t, s1, 100*time.Millisecond) + wg.Wait() } -func TestSwitchPeerFilter(t *testing.T) { +func TestMultiplexSwitch_Peers(t *testing.T) { t.Parallel() var ( - filters = []PeerFilterFunc{ - func(_ IPeerSet, _ Peer) error { return nil }, - func(_ IPeerSet, _ Peer) error { return fmt.Errorf("denied!") }, - func(_ IPeerSet, _ Peer) error { return nil }, - } - sw = MakeSwitch( - cfg, - 1, - "testing", - "123.123.123", - initSwitchFunc, - SwitchPeerFilters(filters...), - ) + peers = mock.GeneratePeers(t, 10) + sw = NewMultiplexSwitch(nil) ) - defer sw.Stop() - - // simulate remote peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() - - p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - isPersistent: sw.isPeerPersistentFn(), - reactorsByCh: sw.reactorsByCh, - }) - if err != nil { - t.Fatal(err) - } - err = sw.addPeer(p) - if err, ok := err.(RejectedError); ok { - if !err.IsFiltered() { - t.Errorf("expected peer to be filtered") - } - } else { - t.Errorf("expected RejectedError") + // Create a new peer set + sw.peers = newSet() + + for _, p := range peers { + // Load it up with peers + sw.peers.Add(p) } -} -func TestSwitchPeerFilterTimeout(t *testing.T) { - t.Parallel() + // Broadcast the data + ps := sw.Peers() - var ( - filters = []PeerFilterFunc{ - func(_ IPeerSet, _ Peer) error { - time.Sleep(10 * time.Millisecond) - return nil - }, - } - sw = MakeSwitch( - cfg, - 1, - "testing", - "123.123.123", - initSwitchFunc, - SwitchFilterTimeout(5*time.Millisecond), - SwitchPeerFilters(filters...), - ) + require.EqualValues( + t, + len(peers), + ps.NumInbound()+ps.NumOutbound(), ) - defer sw.Stop() - - // simulate remote peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() - - p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - isPersistent: sw.isPeerPersistentFn(), - reactorsByCh: sw.reactorsByCh, - }) - if err != nil { - t.Fatal(err) - } - err = sw.addPeer(p) - if _, ok := err.(FilterTimeoutError); !ok { - t.Errorf("expected FilterTimeoutError") + for _, p := range peers { + assert.True(t, ps.Has(p.ID())) } } -func TestSwitchPeerFilterDuplicate(t *testing.T) { +func TestMultiplexSwitch_StopPeer(t *testing.T) { t.Parallel() - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) - sw.Start() - defer sw.Stop() + t.Run("peer not persistent", func(t *testing.T) { + t.Parallel() + + var ( + p = mock.GeneratePeers(t, 1)[0] + mockTransport = &mockTransport{ + removeFn: func(removedPeer Peer) { + assert.Equal(t, p.ID(), removedPeer.ID()) + }, + } + + sw = NewMultiplexSwitch(mockTransport) + ) + + // Create a new peer set + sw.peers = newSet() - // simulate remote peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() + // Save the single peer + sw.peers.Add(p) - p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - isPersistent: sw.isPeerPersistentFn(), - reactorsByCh: sw.reactorsByCh, + // Stop and remove the peer + sw.StopPeerForError(p, nil) + + // Make sure the peer is removed + assert.False(t, sw.peers.Has(p.ID())) }) - if err != nil { - t.Fatal(err) - } - if err := sw.addPeer(p); err != nil { - t.Fatal(err) - } + t.Run("persistent peer", func(t *testing.T) { + t.Parallel() + + var ( + p = mock.GeneratePeers(t, 1)[0] + mockTransport = &mockTransport{ + removeFn: func(removedPeer Peer) { + assert.Equal(t, p.ID(), removedPeer.ID()) + }, + netAddressFn: func() types.NetAddress { + return types.NetAddress{} + }, + } - err = sw.addPeer(p) - if errRej, ok := err.(RejectedError); ok { - if !errRej.IsDuplicate() { - t.Errorf("expected peer to be duplicate. got %v", errRej) + sw = NewMultiplexSwitch(mockTransport) + ) + + // Make sure the peer is persistent + p.IsPersistentFn = func() bool { + return true + } + + p.IsOutboundFn = func() bool { + return false } - } else { - t.Errorf("expected RejectedError, got %v", err) - } -} -func assertNoPeersAfterTimeout(t *testing.T, sw *Switch, timeout time.Duration) { - t.Helper() + // Create a new peer set + sw.peers = newSet() - time.Sleep(timeout) - if sw.Peers().Size() != 0 { - t.Fatalf("Expected %v to not connect to some peers, got %d", sw, sw.Peers().Size()) - } + // Save the single peer + sw.peers.Add(p) + + // Stop and remove the peer + sw.StopPeerForError(p, nil) + + // Make sure the peer is removed + assert.False(t, sw.peers.Has(p.ID())) + + // Make sure the peer is in the dial queue + sw.dialQueue.Has(p.SocketAddr()) + }) } -func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { +func TestMultiplexSwitch_DialLoop(t *testing.T) { t.Parallel() - assert, require := assert.New(t), require.New(t) + t.Run("peer already connected", func(t *testing.T) { + t.Parallel() - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) - err := sw.Start() - if err != nil { - t.Error(err) - } - defer sw.Stop() - - // simulate remote peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() - - p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - isPersistent: sw.isPeerPersistentFn(), - reactorsByCh: sw.reactorsByCh, + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() + + var ( + ch = make(chan struct{}, 1) + + peerDialed bool + + p = mock.GeneratePeers(t, 1)[0] + dialTime = time.Now().Add(-5 * time.Second) // in the past + + mockSet = &mockSet{ + hasFn: func(id types.ID) bool { + require.Equal(t, p.ID(), id) + + cancelFn() + + ch <- struct{}{} + + return true + }, + } + + mockTransport = &mockTransport{ + dialFn: func( + _ context.Context, + _ types.NetAddress, + _ PeerBehavior, + ) (Peer, error) { + peerDialed = true + + return nil, nil + }, + } + + sw = NewMultiplexSwitch(mockTransport) + ) + + sw.peers = mockSet + + // Prepare the dial queue + sw.dialQueue.Push(dial.Item{ + Time: dialTime, + Address: p.SocketAddr(), + }) + + // Run the dial loop + go sw.runDialLoop(ctx) + + select { + case <-ch: + case <-time.After(5 * time.Second): + } + + assert.False(t, peerDialed) }) - require.Nil(err) - err = sw.addPeer(p) - require.Nil(err) + t.Run("peer undialable", func(t *testing.T) { + t.Parallel() - require.NotNil(sw.Peers().Get(rp.ID())) + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() - // simulate failure by closing connection - p.(*peer).CloseConn() + var ( + ch = make(chan struct{}, 1) - assertNoPeersAfterTimeout(t, sw, 100*time.Millisecond) - assert.False(p.IsRunning()) -} + peerDialed bool -func TestSwitchStopPeerForError(t *testing.T) { - t.Parallel() + p = mock.GeneratePeers(t, 1)[0] + dialTime = time.Now().Add(-5 * time.Second) // in the past + + mockSet = &mockSet{ + hasFn: func(id types.ID) bool { + require.Equal(t, p.ID(), id) + + return false + }, + } + + mockTransport = &mockTransport{ + dialFn: func( + _ context.Context, + _ types.NetAddress, + _ PeerBehavior, + ) (Peer, error) { + peerDialed = true + + cancelFn() + + ch <- struct{}{} + + return nil, errors.New("invalid dial") + }, + } + + sw = NewMultiplexSwitch(mockTransport) + ) + + sw.peers = mockSet - // make two connected switches - sw1, sw2 := MakeSwitchPair(t, func(i int, sw *Switch) *Switch { - return initSwitchFunc(i, sw) + // Prepare the dial queue + sw.dialQueue.Push(dial.Item{ + Time: dialTime, + Address: p.SocketAddr(), + }) + + // Run the dial loop + go sw.runDialLoop(ctx) + + select { + case <-ch: + case <-time.After(5 * time.Second): + } + + assert.True(t, peerDialed) }) - assert.Equal(t, len(sw1.Peers().List()), 1) + t.Run("peer dialed and added", func(t *testing.T) { + t.Parallel() - // send messages to the peer from sw1 - p := sw1.Peers().List()[0] - p.Send(0x1, []byte("here's a message to send")) + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() - // stop sw2. this should cause the p to fail, - // which results in calling StopPeerForError internally - sw2.Stop() + var ( + ch = make(chan struct{}, 1) - // now call StopPeerForError explicitly, eg. from a reactor - sw1.StopPeerForError(p, fmt.Errorf("some err")) + peerDialed bool - assert.Equal(t, len(sw1.Peers().List()), 0) -} + p = mock.GeneratePeers(t, 1)[0] + dialTime = time.Now().Add(-5 * time.Second) // in the past -func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { - t.Parallel() + mockTransport = &mockTransport{ + dialFn: func( + _ context.Context, + _ types.NetAddress, + _ PeerBehavior, + ) (Peer, error) { + peerDialed = true - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() - - // 1. simulate failure by closing connection - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() - - err = sw.AddPersistentPeers([]string{rp.Addr().String()}) - require.NoError(t, err) - - err = sw.DialPeerWithAddress(rp.Addr()) - require.Nil(t, err) - require.NotNil(t, sw.Peers().Get(rp.ID())) - - p := sw.Peers().List()[0] - p.(*peer).CloseConn() - - waitUntilSwitchHasAtLeastNPeers(sw, 1) - assert.False(t, p.IsRunning()) // old peer instance - assert.Equal(t, 1, sw.Peers().Size()) // new peer instance - - // 2. simulate first time dial failure - rp = &remotePeer{ - PrivKey: ed25519.GenPrivKey(), - Config: cfg, - // Use different interface to prevent duplicate IP filter, this will break - // beyond two peers. - listenAddr: "127.0.0.1:0", - } - rp.Start() - defer rp.Stop() - - conf := config.DefaultP2PConfig() - conf.TestDialFail = true // will trigger a reconnect - err = sw.addOutboundPeerWithConfig(rp.Addr(), conf) - require.NotNil(t, err) - // DialPeerWithAddres - sw.peerConfig resets the dialer - waitUntilSwitchHasAtLeastNPeers(sw, 2) - assert.Equal(t, 2, sw.Peers().Size()) -} + cancelFn() -func TestSwitchReconnectsToInboundPersistentPeer(t *testing.T) { - t.Parallel() + ch <- struct{}{} + + return p, nil + }, + } - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() + sw = NewMultiplexSwitch(mockTransport) + ) - // 1. simulate failure by closing the connection - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() + // Prepare the dial queue + sw.dialQueue.Push(dial.Item{ + Time: dialTime, + Address: p.SocketAddr(), + }) - err = sw.AddPersistentPeers([]string{rp.Addr().String()}) - require.NoError(t, err) + // Run the dial loop + go sw.runDialLoop(ctx) - conn, err := rp.Dial(sw.NetAddress()) - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - require.NotNil(t, sw.Peers().Get(rp.ID())) + select { + case <-ch: + case <-time.After(5 * time.Second): + } - conn.Close() + require.True(t, sw.Peers().Has(p.ID())) - waitUntilSwitchHasAtLeastNPeers(sw, 1) - assert.Equal(t, 1, sw.Peers().Size()) + assert.True(t, peerDialed) + }) } -func TestSwitchDialPeersAsync(t *testing.T) { +func TestMultiplexSwitch_AcceptLoop(t *testing.T) { t.Parallel() - if testing.Short() { - return - } + t.Run("inbound limit reached", func(t *testing.T) { + t.Parallel() - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() + var ( + ch = make(chan struct{}, 1) + maxInbound = uint64(10) - err = sw.DialPeersAsync([]string{rp.Addr().String()}) - require.NoError(t, err) - time.Sleep(dialRandomizerIntervalMilliseconds * time.Millisecond) - require.NotNil(t, sw.Peers().Get(rp.ID())) -} + peerRemoved bool + + p = mock.GeneratePeers(t, 1)[0] + + mockTransport = &mockTransport{ + acceptFn: func(_ context.Context, _ PeerBehavior) (Peer, error) { + return p, nil + }, + removeFn: func(removedPeer Peer) { + require.Equal(t, p.ID(), removedPeer.ID()) + + peerRemoved = true + + ch <- struct{}{} + }, + } -func waitUntilSwitchHasAtLeastNPeers(sw *Switch, n int) { - for i := 0; i < 20; i++ { - time.Sleep(250 * time.Millisecond) - has := sw.Peers().Size() - if has >= n { - break + ps = &mockSet{ + numInboundFn: func() uint64 { + return maxInbound + }, + } + + sw = NewMultiplexSwitch( + mockTransport, + WithMaxInboundPeers(maxInbound), + ) + ) + + // Set the peer set + sw.peers = ps + + // Run the accept loop + go sw.runAcceptLoop(ctx) + + select { + case <-ch: + case <-time.After(5 * time.Second): } - } + + assert.True(t, peerRemoved) + }) + + t.Run("peer accepted", func(t *testing.T) { + t.Parallel() + + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() + + var ( + ch = make(chan struct{}, 1) + maxInbound = uint64(10) + + peerAdded bool + + p = mock.GeneratePeers(t, 1)[0] + + mockTransport = &mockTransport{ + acceptFn: func(_ context.Context, _ PeerBehavior) (Peer, error) { + return p, nil + }, + } + + ps = &mockSet{ + numInboundFn: func() uint64 { + return maxInbound - 1 // available slot + }, + addFn: func(peer Peer) { + require.Equal(t, p.ID(), peer.ID()) + + peerAdded = true + + ch <- struct{}{} + }, + } + + sw = NewMultiplexSwitch( + mockTransport, + WithMaxInboundPeers(maxInbound), + ) + ) + + // Set the peer set + sw.peers = ps + + // Run the accept loop + go sw.runAcceptLoop(ctx) + + select { + case <-ch: + case <-time.After(5 * time.Second): + } + + assert.True(t, peerAdded) + }) } -func TestSwitchFullConnectivity(t *testing.T) { +func TestMultiplexSwitch_RedialLoop(t *testing.T) { t.Parallel() - switches := MakeConnectedSwitches(cfg, 3, initSwitchFunc, Connect2Switches) - defer func() { - for _, sw := range switches { - sw.Stop() + t.Run("no peers to dial", func(t *testing.T) { + t.Parallel() + + var ( + ch = make(chan struct{}, 1) + + peersChecked = 0 + peers = mock.GeneratePeers(t, 10) + + ps = &mockSet{ + hasFn: func(id types.ID) bool { + exists := false + for _, p := range peers { + if p.ID() == id { + exists = true + + break + } + } + + require.True(t, exists) + + peersChecked++ + + if peersChecked == len(peers) { + ch <- struct{}{} + } + + return true + }, + } + ) + + // Make sure the peers are the + // switch persistent peers + addrs := make([]*types.NetAddress, 0, len(peers)) + + for _, p := range peers { + addrs = append(addrs, p.SocketAddr()) } - }() - for i, sw := range switches { - if sw.Peers().Size() != 2 { - t.Fatalf("Expected each switch to be connected to 2 other, but %d switch only connected to %d", sw.Peers().Size(), i) + // Create the switch + sw := NewMultiplexSwitch( + nil, + WithPersistentPeers(addrs), + ) + + // Set the peer set + sw.peers = ps + + // Run the redial loop + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() + + go sw.runRedialLoop(ctx) + + select { + case <-ch: + case <-time.After(5 * time.Second): } - } -} -func TestSwitchAcceptRoutine(t *testing.T) { - t.Parallel() + assert.Equal(t, len(peers), peersChecked) + }) + + t.Run("missing peers dialed", func(t *testing.T) { + t.Parallel() + + var ( + peers = mock.GeneratePeers(t, 10) + missingPeer = peers[0] + missingAddr = missingPeer.SocketAddr() + + peersDialed []types.NetAddress + + mockTransport = &mockTransport{ + dialFn: func( + _ context.Context, + address types.NetAddress, + _ PeerBehavior, + ) (Peer, error) { + peersDialed = append(peersDialed, address) + + if address.Equals(*missingPeer.SocketAddr()) { + return missingPeer, nil + } + + return nil, errors.New("invalid dial") + }, + } + ps = &mockSet{ + hasFn: func(id types.ID) bool { + return id != missingPeer.ID() + }, + } + ) + + // Make sure the peers are the + // switch persistent peers + addrs := make([]*types.NetAddress, 0, len(peers)) + + for _, p := range peers { + addrs = append(addrs, p.SocketAddr()) + } + + // Create the switch + sw := NewMultiplexSwitch( + mockTransport, + WithPersistentPeers(addrs), + ) + + // Set the peer set + sw.peers = ps + + // Run the redial loop + ctx, cancelFn := context.WithTimeout( + context.Background(), + 5*time.Second, + ) + defer cancelFn() + + var wg sync.WaitGroup + + wg.Add(2) + + go func() { + defer wg.Done() + + sw.runRedialLoop(ctx) + }() + + go func() { + defer wg.Done() + + deadline := time.After(5 * time.Second) - cfg.MaxNumInboundPeers = 5 - - // make switch - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() - - remotePeers := make([]*remotePeer, 0) - assert.Equal(t, 0, sw.Peers().Size()) - - // 1. check we connect up to MaxNumInboundPeers - for i := 0; i < cfg.MaxNumInboundPeers; i++ { - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - remotePeers = append(remotePeers, rp) - rp.Start() - c, err := rp.Dial(sw.NetAddress()) - require.NoError(t, err) - // spawn a reading routine to prevent connection from closing - go func(c net.Conn) { for { - one := make([]byte, 1) - _, err := c.Read(one) - if err != nil { + select { + case <-deadline: + return + default: + if !sw.dialQueue.Has(missingAddr) { + continue + } + + cancelFn() + return } } - }(c) - } - time.Sleep(100 * time.Millisecond) - assert.Equal(t, cfg.MaxNumInboundPeers, sw.Peers().Size()) - - // 2. check we close new connections if we already have MaxNumInboundPeers peers - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - conn, err := rp.Dial(sw.NetAddress()) - require.NoError(t, err) - // check conn is closed - one := make([]byte, 1) - conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - _, err = conn.Read(one) - assert.Equal(t, io.EOF, err) - assert.Equal(t, cfg.MaxNumInboundPeers, sw.Peers().Size()) - rp.Stop() - - // stop remote peers - for _, rp := range remotePeers { - rp.Stop() - } -} + }() -type errorTransport struct { - acceptErr error -} + wg.Wait() -func (et errorTransport) NetAddress() NetAddress { - panic("not implemented") + require.True(t, sw.dialQueue.Has(missingAddr)) + assert.Equal(t, missingAddr, sw.dialQueue.Peek().Address) + }) } -func (et errorTransport) Accept(c peerConfig) (Peer, error) { - return nil, et.acceptErr -} +func TestMultiplexSwitch_DialPeers(t *testing.T) { + t.Parallel() -func (errorTransport) Dial(NetAddress, peerConfig) (Peer, error) { - panic("not implemented") -} + t.Run("self dial request", func(t *testing.T) { + t.Parallel() -func (errorTransport) Cleanup(Peer) { - panic("not implemented") -} + var ( + p = mock.GeneratePeers(t, 1)[0] + addr = types.NetAddress{ + ID: "id", + IP: p.SocketAddr().IP, + Port: p.SocketAddr().Port, + } -func TestSwitchAcceptRoutineErrorCases(t *testing.T) { - t.Parallel() + mockTransport = &mockTransport{ + netAddressFn: func() types.NetAddress { + return addr + }, + } + ) - sw := NewSwitch(cfg, errorTransport{FilterTimeoutError{}}) - assert.NotPanics(t, func() { - err := sw.Start() - assert.NoError(t, err) - sw.Stop() - }) + // Make sure the "peer" has the same address + // as the transport (node) + p.NodeInfoFn = func() types.NodeInfo { + return types.NodeInfo{ + PeerID: addr.ID, + } + } - sw = NewSwitch(cfg, errorTransport{RejectedError{conn: nil, err: errors.New("filtered"), isFiltered: true}}) - assert.NotPanics(t, func() { - err := sw.Start() - assert.NoError(t, err) - sw.Stop() - }) + sw := NewMultiplexSwitch(mockTransport) + + // Dial the peers + sw.DialPeers(p.SocketAddr()) - sw = NewSwitch(cfg, errorTransport{TransportClosedError{}}) - assert.NotPanics(t, func() { - err := sw.Start() - assert.NoError(t, err) - sw.Stop() + // Make sure the peer wasn't actually dialed + assert.False(t, sw.dialQueue.Has(p.SocketAddr())) }) -} -// mockReactor checks that InitPeer never called before RemovePeer. If that's -// not true, InitCalledBeforeRemoveFinished will return true. -type mockReactor struct { - *BaseReactor + t.Run("outbound peer limit reached", func(t *testing.T) { + t.Parallel() - // atomic - removePeerInProgress uint32 - initCalledBeforeRemoveFinished uint32 -} + var ( + maxOutbound = uint64(10) + peers = mock.GeneratePeers(t, 10) -func (r *mockReactor) RemovePeer(peer Peer, reason interface{}) { - atomic.StoreUint32(&r.removePeerInProgress, 1) - defer atomic.StoreUint32(&r.removePeerInProgress, 0) - time.Sleep(100 * time.Millisecond) -} + mockTransport = &mockTransport{ + netAddressFn: func() types.NetAddress { + return types.NetAddress{ + ID: "id", + IP: net.IP{}, + } + }, + } -func (r *mockReactor) InitPeer(peer Peer) Peer { - if atomic.LoadUint32(&r.removePeerInProgress) == 1 { - atomic.StoreUint32(&r.initCalledBeforeRemoveFinished, 1) - } + ps = &mockSet{ + numOutboundFn: func() uint64 { + return maxOutbound + }, + } + ) - return peer -} + sw := NewMultiplexSwitch( + mockTransport, + WithMaxOutboundPeers(maxOutbound), + ) -func (r *mockReactor) InitCalledBeforeRemoveFinished() bool { - return atomic.LoadUint32(&r.initCalledBeforeRemoveFinished) == 1 -} + // Set the peer set + sw.peers = ps -// see stopAndRemovePeer -func TestFlappySwitchInitPeerIsNotCalledBeforeRemovePeer(t *testing.T) { - t.Parallel() + // Dial the peers + addrs := make([]*types.NetAddress, 0, len(peers)) - testutils.FilterStability(t, testutils.Flappy) + for _, p := range peers { + addrs = append(addrs, p.SocketAddr()) + } - // make reactor - reactor := &mockReactor{} - reactor.BaseReactor = NewBaseReactor("mockReactor", reactor) + sw.DialPeers(addrs...) - // make switch - sw := MakeSwitch(cfg, 1, "testing", "123.123.123", func(i int, sw *Switch) *Switch { - sw.AddReactor("mock", reactor) - return sw + // Make sure no peers were dialed + for _, p := range peers { + assert.False(t, sw.dialQueue.Has(p.SocketAddr())) + } }) - err := sw.Start() - require.NoError(t, err) - defer sw.Stop() - - // add peer - rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} - rp.Start() - defer rp.Stop() - _, err = rp.Dial(sw.NetAddress()) - require.NoError(t, err) - // wait till the switch adds rp to the peer set - time.Sleep(100 * time.Millisecond) - - // stop peer asynchronously - go sw.StopPeerForError(sw.Peers().Get(rp.ID()), "test") - - // simulate peer reconnecting to us - _, err = rp.Dial(sw.NetAddress()) - require.NoError(t, err) - // wait till the switch adds rp to the peer set - time.Sleep(100 * time.Millisecond) - - // make sure reactor.RemovePeer is finished before InitPeer is called - assert.False(t, reactor.InitCalledBeforeRemoveFinished()) -} -func BenchmarkSwitchBroadcast(b *testing.B) { - s1, s2 := MakeSwitchPair(b, func(i int, sw *Switch) *Switch { - // Make bar reactors of bar channels each - sw.AddReactor("foo", NewTestReactor([]*conn.ChannelDescriptor{ - {ID: byte(0x00), Priority: 10}, - {ID: byte(0x01), Priority: 10}, - }, false)) - sw.AddReactor("bar", NewTestReactor([]*conn.ChannelDescriptor{ - {ID: byte(0x02), Priority: 10}, - {ID: byte(0x03), Priority: 10}, - }, false)) - return sw - }) - defer s1.Stop() - defer s2.Stop() + t.Run("peers dialed", func(t *testing.T) { + t.Parallel() + + var ( + maxOutbound = uint64(1000) + peers = mock.GeneratePeers(t, int(maxOutbound/2)) - // Allow time for goroutines to boot up - time.Sleep(1 * time.Second) + mockTransport = &mockTransport{ + netAddressFn: func() types.NetAddress { + return types.NetAddress{ + ID: "id", + IP: net.IP{}, + } + }, + } + ) - b.ResetTimer() + sw := NewMultiplexSwitch( + mockTransport, + WithMaxOutboundPeers(10), + ) - numSuccess, numFailure := 0, 0 + // Dial the peers + addrs := make([]*types.NetAddress, 0, len(peers)) - // Send random message from foo channel to another - for i := 0; i < b.N; i++ { - chID := byte(i % 4) - successChan := s1.Broadcast(chID, []byte("test data")) - for s := range successChan { - if s { - numSuccess++ - } else { - numFailure++ - } + for _, p := range peers { + addrs = append(addrs, p.SocketAddr()) } - } - b.Logf("success: %v, failure: %v", numSuccess, numFailure) + sw.DialPeers(addrs...) + + // Make sure peers were dialed + for _, p := range peers { + assert.True(t, sw.dialQueue.Has(p.SocketAddr())) + } + }) } diff --git a/tm2/pkg/p2p/test_util.go b/tm2/pkg/p2p/test_util.go deleted file mode 100644 index dd0d9cd6bc7..00000000000 --- a/tm2/pkg/p2p/test_util.go +++ /dev/null @@ -1,238 +0,0 @@ -package p2p - -import ( - "fmt" - "net" - "time" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" - "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p/config" - "github.com/gnolang/gno/tm2/pkg/p2p/conn" - "github.com/gnolang/gno/tm2/pkg/random" - "github.com/gnolang/gno/tm2/pkg/versionset" -) - -const testCh = 0x01 - -// ------------------------------------------------ - -func CreateRoutableAddr() (addr string, netAddr *NetAddress) { - for { - id := ed25519.GenPrivKey().PubKey().Address().ID() - var err error - addr = fmt.Sprintf("%s@%v.%v.%v.%v:26656", id, random.RandInt()%256, random.RandInt()%256, random.RandInt()%256, random.RandInt()%256) - netAddr, err = NewNetAddressFromString(addr) - if err != nil { - panic(err) - } - if netAddr.Routable() { - break - } - } - return -} - -// ------------------------------------------------------------------ -// Connects switches via arbitrary net.Conn. Used for testing. - -const TEST_HOST = "localhost" - -// MakeConnectedSwitches returns n switches, connected according to the connect func. -// If connect==Connect2Switches, the switches will be fully connected. -// initSwitch defines how the i'th switch should be initialized (ie. with what reactors). -// NOTE: panics if any switch fails to start. -func MakeConnectedSwitches(cfg *config.P2PConfig, n int, initSwitch func(int, *Switch) *Switch, connect func([]*Switch, int, int)) []*Switch { - switches := make([]*Switch, n) - for i := 0; i < n; i++ { - switches[i] = MakeSwitch(cfg, i, TEST_HOST, "123.123.123", initSwitch) - } - - if err := StartSwitches(switches); err != nil { - panic(err) - } - - for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - connect(switches, i, j) - } - } - - return switches -} - -// Connect2Switches will connect switches i and j via net.Pipe(). -// Blocks until a connection is established. -// NOTE: caller ensures i and j are within bounds. -func Connect2Switches(switches []*Switch, i, j int) { - switchI := switches[i] - switchJ := switches[j] - - c1, c2 := conn.NetPipe() - - doneCh := make(chan struct{}) - go func() { - err := switchI.addPeerWithConnection(c1) - if err != nil { - panic(err) - } - doneCh <- struct{}{} - }() - go func() { - err := switchJ.addPeerWithConnection(c2) - if err != nil { - panic(err) - } - doneCh <- struct{}{} - }() - <-doneCh - <-doneCh -} - -func (sw *Switch) addPeerWithConnection(conn net.Conn) error { - pc, err := testInboundPeerConn(conn, sw.config, sw.nodeKey.PrivKey) - if err != nil { - if err := conn.Close(); err != nil { - sw.Logger.Error("Error closing connection", "err", err) - } - return err - } - - ni, err := handshake(conn, time.Second, sw.nodeInfo) - if err != nil { - if err := conn.Close(); err != nil { - sw.Logger.Error("Error closing connection", "err", err) - } - return err - } - - p := newPeer( - pc, - MConnConfig(sw.config), - ni, - sw.reactorsByCh, - sw.chDescs, - sw.StopPeerForError, - ) - - if err = sw.addPeer(p); err != nil { - pc.CloseConn() - return err - } - - return nil -} - -// StartSwitches calls sw.Start() for each given switch. -// It returns the first encountered error. -func StartSwitches(switches []*Switch) error { - for _, s := range switches { - err := s.Start() // start switch and reactors - if err != nil { - return err - } - } - return nil -} - -func MakeSwitch( - cfg *config.P2PConfig, - i int, - network, version string, - initSwitch func(int, *Switch) *Switch, - opts ...SwitchOption, -) *Switch { - nodeKey := NodeKey{ - PrivKey: ed25519.GenPrivKey(), - } - nodeInfo := testNodeInfo(nodeKey.ID(), fmt.Sprintf("node%d", i)) - - t := NewMultiplexTransport(nodeInfo, nodeKey, MConnConfig(cfg)) - - if err := t.Listen(*nodeInfo.NetAddress); err != nil { - panic(err) - } - - // TODO: let the config be passed in? - sw := initSwitch(i, NewSwitch(cfg, t, opts...)) - sw.SetLogger(log.NewNoopLogger().With("switch", i)) - sw.SetNodeKey(&nodeKey) - - for ch := range sw.reactorsByCh { - nodeInfo.Channels = append(nodeInfo.Channels, ch) - } - - // TODO: We need to setup reactors ahead of time so the NodeInfo is properly - // populated and we don't have to do those awkward overrides and setters. - t.nodeInfo = nodeInfo - sw.SetNodeInfo(nodeInfo) - - return sw -} - -func testInboundPeerConn( - conn net.Conn, - config *config.P2PConfig, - ourNodePrivKey crypto.PrivKey, -) (peerConn, error) { - return testPeerConn(conn, config, false, false, ourNodePrivKey, nil) -} - -func testPeerConn( - rawConn net.Conn, - cfg *config.P2PConfig, - outbound, persistent bool, - ourNodePrivKey crypto.PrivKey, - socketAddr *NetAddress, -) (pc peerConn, err error) { - conn := rawConn - - // Fuzz connection - if cfg.TestFuzz { - // so we have time to do peer handshakes and get set up - conn = FuzzConnAfterFromConfig(conn, 10*time.Second, cfg.TestFuzzConfig) - } - - // Encrypt connection - conn, err = upgradeSecretConn(conn, cfg.HandshakeTimeout, ourNodePrivKey) - if err != nil { - return pc, errors.Wrap(err, "Error creating peer") - } - - // Only the information we already have - return newPeerConn(outbound, persistent, conn, socketAddr), nil -} - -// ---------------------------------------------------------------- -// rand node info - -func testNodeInfo(id ID, name string) NodeInfo { - return testNodeInfoWithNetwork(id, name, "testing") -} - -func testVersionSet() versionset.VersionSet { - return versionset.VersionSet{ - versionset.VersionInfo{ - Name: "p2p", - Version: "v0.0.0", // dontcare - }, - } -} - -func testNodeInfoWithNetwork(id ID, name, network string) NodeInfo { - return NodeInfo{ - VersionSet: testVersionSet(), - NetAddress: NewNetAddressFromIPPort(id, net.ParseIP("127.0.0.1"), 0), - Network: network, - Software: "p2ptest", - Version: "v1.2.3-rc.0-deadbeef", - Channels: []byte{testCh}, - Moniker: name, - Other: NodeInfoOther{ - TxIndex: "on", - RPCAddress: fmt.Sprintf("127.0.0.1:%d", 0), - }, - } -} diff --git a/tm2/pkg/p2p/transport.go b/tm2/pkg/p2p/transport.go index 5bfae9e52b8..d53b64fc06f 100644 --- a/tm2/pkg/p2p/transport.go +++ b/tm2/pkg/p2p/transport.go @@ -3,144 +3,64 @@ package p2p import ( "context" "fmt" + "io" + "log/slog" "net" - "strconv" + "sync" "time" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "golang.org/x/sync/errgroup" ) -const ( - defaultDialTimeout = time.Second - defaultFilterTimeout = 5 * time.Second - defaultHandshakeTimeout = 3 * time.Second -) - -// IPResolver is a behaviour subset of net.Resolver. -type IPResolver interface { - LookupIPAddr(context.Context, string) ([]net.IPAddr, error) -} - -// accept is the container to carry the upgraded connection and NodeInfo from an -// asynchronously running routine to the Accept method. -type accept struct { - netAddr *NetAddress - conn net.Conn - nodeInfo NodeInfo - err error -} - -// peerConfig is used to bundle data we need to fully setup a Peer with an -// MConn, provided by the caller of Accept and Dial (currently the Switch). This -// a temporary measure until reactor setup is less dynamic and we introduce the -// concept of PeerBehaviour to communicate about significant Peer lifecycle -// events. -// TODO(xla): Refactor out with more static Reactor setup and PeerBehaviour. -type peerConfig struct { - chDescs []*conn.ChannelDescriptor - onPeerError func(Peer, interface{}) - outbound bool - // isPersistent allows you to set a function, which, given socket address - // (for outbound peers) OR self-reported address (for inbound peers), tells - // if the peer is persistent or not. - isPersistent func(*NetAddress) bool - reactorsByCh map[byte]Reactor -} - -// Transport emits and connects to Peers. The implementation of Peer is left to -// the transport. Each transport is also responsible to filter establishing -// peers specific to its domain. -type Transport interface { - // Listening address. - NetAddress() NetAddress - - // Accept returns a newly connected Peer. - Accept(peerConfig) (Peer, error) - - // Dial connects to the Peer for the address. - Dial(NetAddress, peerConfig) (Peer, error) - - // Cleanup any resources associated with Peer. - Cleanup(Peer) -} - -// TransportLifecycle bundles the methods for callers to control start and stop -// behaviour. -type TransportLifecycle interface { - Close() error - Listen(NetAddress) error -} - -// ConnFilterFunc to be implemented by filter hooks after a new connection has -// been established. The set of existing connections is passed along together -// with all resolved IPs for the new connection. -type ConnFilterFunc func(ConnSet, net.Conn, []net.IP) error - -// ConnDuplicateIPFilter resolves and keeps all ips for an incoming connection -// and refuses new ones if they come from a known ip. -func ConnDuplicateIPFilter() ConnFilterFunc { - return func(cs ConnSet, c net.Conn, ips []net.IP) error { - for _, ip := range ips { - if cs.HasIP(ip) { - return RejectedError{ - conn: c, - err: fmt.Errorf("IP<%v> already connected", ip), - isDuplicate: true, - } - } - } +// defaultHandshakeTimeout is the timeout for the STS handshaking protocol +const defaultHandshakeTimeout = 3 * time.Second - return nil - } -} +var ( + errTransportClosed = errors.New("transport is closed") + errTransportInactive = errors.New("transport is inactive") + errDuplicateConnection = errors.New("duplicate peer connection") + errPeerIDNodeInfoMismatch = errors.New("connection ID does not match node info ID") + errPeerIDDialMismatch = errors.New("connection ID does not match dialed ID") + errIncompatibleNodeInfo = errors.New("incompatible node info") +) -// MultiplexTransportOption sets an optional parameter on the -// MultiplexTransport. -type MultiplexTransportOption func(*MultiplexTransport) +type connUpgradeFn func(io.ReadWriteCloser, crypto.PrivKey) (*conn.SecretConnection, error) -// MultiplexTransportConnFilters sets the filters for rejection new connections. -func MultiplexTransportConnFilters( - filters ...ConnFilterFunc, -) MultiplexTransportOption { - return func(mt *MultiplexTransport) { mt.connFilters = filters } -} +type secretConn interface { + net.Conn -// MultiplexTransportFilterTimeout sets the timeout waited for filter calls to -// return. -func MultiplexTransportFilterTimeout( - timeout time.Duration, -) MultiplexTransportOption { - return func(mt *MultiplexTransport) { mt.filterTimeout = timeout } + RemotePubKey() crypto.PubKey } -// MultiplexTransportResolver sets the Resolver used for ip lookups, defaults to -// net.DefaultResolver. -func MultiplexTransportResolver(resolver IPResolver) MultiplexTransportOption { - return func(mt *MultiplexTransport) { mt.resolver = resolver } +// peerInfo is a wrapper for an unverified peer connection +type peerInfo struct { + addr *types.NetAddress // the dial address of the peer + conn net.Conn // the connection associated with the peer + nodeInfo types.NodeInfo // the relevant peer node info } // MultiplexTransport accepts and dials tcp connections and upgrades them to // multiplexed peers. type MultiplexTransport struct { - netAddr NetAddress - listener net.Listener + ctx context.Context + cancelFn context.CancelFunc - acceptc chan accept - closec chan struct{} + logger *slog.Logger - // Lookup table for duplicate ip and id checks. - conns ConnSet - connFilters []ConnFilterFunc + netAddr types.NetAddress // the node's P2P dial address, used for handshaking + nodeInfo types.NodeInfo // the node's P2P info, used for handshaking + nodeKey types.NodeKey // the node's private P2P key, used for handshaking - dialTimeout time.Duration - filterTimeout time.Duration - handshakeTimeout time.Duration - nodeInfo NodeInfo - nodeKey NodeKey - resolver IPResolver + listener net.Listener // listener for inbound peer connections + peerCh chan peerInfo // pipe for inbound peer connections + activeConns sync.Map // active peer connections (remote address -> nothing) + + connUpgradeFn connUpgradeFn // Upgrades the connection to a secret connection // TODO(xla): This config is still needed as we parameterize peerConn and // peer currently. All relevant configuration should be refactored into options @@ -148,439 +68,376 @@ type MultiplexTransport struct { mConfig conn.MConnConfig } -// Test multiplexTransport for interface completeness. -var ( - _ Transport = (*MultiplexTransport)(nil) - _ TransportLifecycle = (*MultiplexTransport)(nil) -) - // NewMultiplexTransport returns a tcp connected multiplexed peer. func NewMultiplexTransport( - nodeInfo NodeInfo, - nodeKey NodeKey, + nodeInfo types.NodeInfo, + nodeKey types.NodeKey, mConfig conn.MConnConfig, + logger *slog.Logger, ) *MultiplexTransport { return &MultiplexTransport{ - acceptc: make(chan accept), - closec: make(chan struct{}), - dialTimeout: defaultDialTimeout, - filterTimeout: defaultFilterTimeout, - handshakeTimeout: defaultHandshakeTimeout, - mConfig: mConfig, - nodeInfo: nodeInfo, - nodeKey: nodeKey, - conns: NewConnSet(), - resolver: net.DefaultResolver, + peerCh: make(chan peerInfo, 1), + mConfig: mConfig, + nodeInfo: nodeInfo, + nodeKey: nodeKey, + logger: logger, + connUpgradeFn: conn.MakeSecretConnection, } } -// NetAddress implements Transport. -func (mt *MultiplexTransport) NetAddress() NetAddress { +// NetAddress returns the transport's listen address (for p2p connections) +func (mt *MultiplexTransport) NetAddress() types.NetAddress { return mt.netAddr } -// Accept implements Transport. -func (mt *MultiplexTransport) Accept(cfg peerConfig) (Peer, error) { +// Accept waits for a verified inbound Peer to connect, and returns it [BLOCKING] +func (mt *MultiplexTransport) Accept(ctx context.Context, behavior PeerBehavior) (Peer, error) { + // Sanity check, no need to wait + // on an inactive transport + if mt.listener == nil { + return nil, errTransportInactive + } + select { - // This case should never have any side-effectful/blocking operations to - // ensure that quality peers are ready to be used. - case a := <-mt.acceptc: - if a.err != nil { - return nil, a.err + case <-ctx.Done(): + return nil, ctx.Err() + case info, ok := <-mt.peerCh: + if !ok { + return nil, errTransportClosed } - cfg.outbound = false - - return mt.wrapPeer(a.conn, a.nodeInfo, cfg, a.netAddr), nil - case <-mt.closec: - return nil, TransportClosedError{} + return mt.newMultiplexPeer(info, behavior, false) } } -// Dial implements Transport. +// Dial creates an outbound Peer connection, and +// verifies it (performs handshaking) [BLOCKING] func (mt *MultiplexTransport) Dial( - addr NetAddress, - cfg peerConfig, + ctx context.Context, + addr types.NetAddress, + behavior PeerBehavior, ) (Peer, error) { - c, err := addr.DialTimeout(mt.dialTimeout) + // Set a dial timeout for the connection + c, err := addr.DialContext(ctx) if err != nil { return nil, err } - // TODO(xla): Evaluate if we should apply filters if we explicitly dial. - if err := mt.filterConn(c); err != nil { - return nil, err - } - - secretConn, nodeInfo, err := mt.upgrade(c, &addr) + // Process the connection with expected ID + info, err := mt.processConn(c, addr.ID) if err != nil { - return nil, err - } - - cfg.outbound = true + // Close the net peer connection + _ = c.Close() - p := mt.wrapPeer(secretConn, nodeInfo, cfg, &addr) + return nil, fmt.Errorf("unable to process connection, %w", err) + } - return p, nil + return mt.newMultiplexPeer(info, behavior, true) } -// Close implements TransportLifecycle. +// Close stops the multiplex transport func (mt *MultiplexTransport) Close() error { - close(mt.closec) - - if mt.listener != nil { - return mt.listener.Close() + if mt.listener == nil { + return nil } - return nil + mt.cancelFn() + + return mt.listener.Close() } -// Listen implements TransportLifecycle. -func (mt *MultiplexTransport) Listen(addr NetAddress) error { +// Listen starts an active process of listening for incoming connections [NON-BLOCKING] +func (mt *MultiplexTransport) Listen(addr types.NetAddress) error { + // Reserve a port, and start listening ln, err := net.Listen("tcp", addr.DialString()) if err != nil { - return err + return fmt.Errorf("unable to listen on address, %w", err) } if addr.Port == 0 { // net.Listen on port 0 means the kernel will auto-allocate a port // - find out which one has been given to us. - _, p, err := net.SplitHostPort(ln.Addr().String()) - if err != nil { + tcpAddr, ok := ln.Addr().(*net.TCPAddr) + if !ok { return fmt.Errorf("error finding port (after listening on port 0): %w", err) } - pInt, _ := strconv.Atoi(p) - addr.Port = uint16(pInt) + + addr.Port = uint16(tcpAddr.Port) } + // Set up the context + mt.ctx, mt.cancelFn = context.WithCancel(context.Background()) + mt.netAddr = addr mt.listener = ln - go mt.acceptPeers() + // Run the routine for accepting + // incoming peer connections + go mt.runAcceptLoop() return nil } -func (mt *MultiplexTransport) acceptPeers() { +// runAcceptLoop runs the loop where incoming peers are: +// +// 1. accepted by the transport +// 2. filtered +// 3. upgraded (handshaked + verified) +func (mt *MultiplexTransport) runAcceptLoop() { + var wg sync.WaitGroup + + defer func() { + wg.Wait() // Wait for all process routines + + close(mt.peerCh) + }() + for { - c, err := mt.listener.Accept() - if err != nil { - // If Close() has been called, silently exit. - select { - case _, ok := <-mt.closec: - if !ok { - return - } - default: - // Transport is not closed - } + select { + case <-mt.ctx.Done(): + mt.logger.Debug("transport accept context closed") - mt.acceptc <- accept{err: err} return - } + default: + // Accept an incoming peer connection + c, err := mt.listener.Accept() + if err != nil { + mt.logger.Error( + "unable to accept p2p connection", + "err", err, + ) - // Connection upgrade and filtering should be asynchronous to avoid - // Head-of-line blocking[0]. - // Reference: https://github.com/tendermint/classic/issues/2047 - // - // [0] https://en.wikipedia.org/wiki/Head-of-line_blocking - go func(c net.Conn) { - defer func() { - if r := recover(); r != nil { - err := RejectedError{ - conn: c, - err: errors.New("recovered from panic: %v", r), - isAuthFailure: true, - } - select { - case mt.acceptc <- accept{err: err}: - case <-mt.closec: - // Give up if the transport was closed. - _ = c.Close() - return - } - } - }() - - var ( - nodeInfo NodeInfo - secretConn *conn.SecretConnection - netAddr *NetAddress - ) - - err := mt.filterConn(c) - if err == nil { - secretConn, nodeInfo, err = mt.upgrade(c, nil) - if err == nil { - addr := c.RemoteAddr() - id := secretConn.RemotePubKey().Address().ID() - netAddr = NewNetAddress(id, addr) - } + continue } - select { - case mt.acceptc <- accept{netAddr, secretConn, nodeInfo, err}: - // Make the upgraded peer available. - case <-mt.closec: - // Give up if the transport was closed. - _ = c.Close() - return - } - }(c) - } -} + // Process the new connection asynchronously + wg.Add(1) -// Cleanup removes the given address from the connections set and -// closes the connection. -func (mt *MultiplexTransport) Cleanup(p Peer) { - mt.conns.RemoveAddr(p.RemoteAddr()) - _ = p.CloseConn() -} + go func(c net.Conn) { + defer wg.Done() -func (mt *MultiplexTransport) cleanup(c net.Conn) error { - mt.conns.Remove(c) + info, err := mt.processConn(c, "") + if err != nil { + mt.logger.Error( + "unable to process p2p connection", + "err", err, + ) - return c.Close() -} + // Close the connection + _ = c.Close() -func (mt *MultiplexTransport) filterConn(c net.Conn) (err error) { - defer func() { - if err != nil { - _ = c.Close() - } - }() + return + } - // Reject if connection is already present. - if mt.conns.Has(c) { - return RejectedError{conn: c, isDuplicate: true} + select { + case mt.peerCh <- info: + case <-mt.ctx.Done(): + // Give up if the transport was closed. + _ = c.Close() + } + }(c) + } } +} - // Resolve ips for incoming conn. - ips, err := resolveIPs(mt.resolver, c) - if err != nil { - return err +// processConn handles the raw connection by upgrading it and verifying it +func (mt *MultiplexTransport) processConn(c net.Conn, expectedID types.ID) (peerInfo, error) { + dialAddr := c.RemoteAddr().String() + + // Check if the connection is a duplicate one + if _, exists := mt.activeConns.LoadOrStore(dialAddr, struct{}{}); exists { + return peerInfo{}, errDuplicateConnection } - errc := make(chan error, len(mt.connFilters)) + // Handshake with the peer, through STS + secretConn, nodeInfo, err := mt.upgradeAndVerifyConn(c) + if err != nil { + mt.activeConns.Delete(dialAddr) - for _, f := range mt.connFilters { - go func(f ConnFilterFunc, c net.Conn, ips []net.IP, errc chan<- error) { - errc <- f(mt.conns, c, ips) - }(f, c, ips, errc) + return peerInfo{}, fmt.Errorf("unable to upgrade connection, %w", err) } - for i := 0; i < cap(errc); i++ { - select { - case err := <-errc: - if err != nil { - return RejectedError{conn: c, err: err, isFiltered: true} - } - case <-time.After(mt.filterTimeout): - return FilterTimeoutError{} - } + // Grab the connection ID. + // At this point, the connection and information shared + // with the peer is considered valid, since full handshaking + // and verification took place + id := secretConn.RemotePubKey().Address().ID() + + // The reason the dial ID needs to be verified is because + // for outbound peers (peers the node dials), there is an expected peer ID + // when initializing the outbound connection, that can differ from the exchanged one. + // For inbound peers, the ID is whatever the peer exchanges during the + // handshaking process, and is verified separately + if !expectedID.IsZero() && id.String() != expectedID.String() { + mt.activeConns.Delete(dialAddr) + + return peerInfo{}, fmt.Errorf( + "%w (expected %q got %q)", + errPeerIDDialMismatch, + expectedID, + id, + ) } - mt.conns.Set(c, ips) + netAddr, _ := types.NewNetAddress(id, c.RemoteAddr()) - return nil + return peerInfo{ + addr: netAddr, + conn: secretConn, + nodeInfo: nodeInfo, + }, nil } -func (mt *MultiplexTransport) upgrade( - c net.Conn, - dialedAddr *NetAddress, -) (secretConn *conn.SecretConnection, nodeInfo NodeInfo, err error) { - defer func() { - if err != nil { - _ = mt.cleanup(c) - } - }() +// Remove removes the peer resources from the transport +func (mt *MultiplexTransport) Remove(p Peer) { + mt.activeConns.Delete(p.RemoteAddr().String()) +} - secretConn, err = upgradeSecretConn(c, mt.handshakeTimeout, mt.nodeKey.PrivKey) +// upgradeAndVerifyConn upgrades the connections (performs the handshaking process) +// and verifies that the connecting peer is valid +func (mt *MultiplexTransport) upgradeAndVerifyConn(c net.Conn) (secretConn, types.NodeInfo, error) { + // Upgrade to a secret connection. + // A secret connection is a connection that has passed + // an initial handshaking process, as defined by the STS + // protocol, and is considered to be secure and authentic + sc, err := mt.upgradeToSecretConn( + c, + defaultHandshakeTimeout, + mt.nodeKey.PrivKey, + ) if err != nil { - return nil, NodeInfo{}, RejectedError{ - conn: c, - err: fmt.Errorf("secret conn failed: %w", err), - isAuthFailure: true, - } - } - - // For outgoing conns, ensure connection key matches dialed key. - connID := secretConn.RemotePubKey().Address().ID() - if dialedAddr != nil { - if dialedID := dialedAddr.ID; connID.String() != dialedID.String() { - return nil, NodeInfo{}, RejectedError{ - conn: c, - id: connID, - err: fmt.Errorf( - "conn.ID (%v) dialed ID (%v) mismatch", - connID, - dialedID, - ), - isAuthFailure: true, - } - } + return nil, types.NodeInfo{}, fmt.Errorf("unable to upgrade p2p connection, %w", err) } - nodeInfo, err = handshake(secretConn, mt.handshakeTimeout, mt.nodeInfo) + // Exchange node information + nodeInfo, err := exchangeNodeInfo(sc, defaultHandshakeTimeout, mt.nodeInfo) if err != nil { - return nil, NodeInfo{}, RejectedError{ - conn: c, - err: fmt.Errorf("handshake failed: %w", err), - isAuthFailure: true, - } + return nil, types.NodeInfo{}, fmt.Errorf("unable to exchange node information, %w", err) } - if err := nodeInfo.Validate(); err != nil { - return nil, NodeInfo{}, RejectedError{ - conn: c, - err: err, - isNodeInfoInvalid: true, - } - } + // Ensure the connection ID matches the node's reported ID + connID := sc.RemotePubKey().Address().ID() - // Ensure connection key matches self reported key. if connID != nodeInfo.ID() { - return nil, NodeInfo{}, RejectedError{ - conn: c, - id: connID, - err: fmt.Errorf( - "conn.ID (%v) NodeInfo.ID (%v) mismatch", - connID, - nodeInfo.ID(), - ), - isAuthFailure: true, - } - } - - // Reject self. - if mt.nodeInfo.ID() == nodeInfo.ID() { - return nil, NodeInfo{}, RejectedError{ - addr: *NewNetAddress(nodeInfo.ID(), c.RemoteAddr()), - conn: c, - id: nodeInfo.ID(), - isSelf: true, - } + return nil, types.NodeInfo{}, fmt.Errorf( + "%w (expected %q got %q)", + errPeerIDNodeInfoMismatch, + connID.String(), + nodeInfo.ID().String(), + ) } - if err := mt.nodeInfo.CompatibleWith(nodeInfo); err != nil { - return nil, NodeInfo{}, RejectedError{ - conn: c, - err: err, - id: nodeInfo.ID(), - isIncompatible: true, - } + // Check compatibility with the node + if err = mt.nodeInfo.CompatibleWith(nodeInfo); err != nil { + return nil, types.NodeInfo{}, fmt.Errorf("%w, %w", errIncompatibleNodeInfo, err) } - return secretConn, nodeInfo, nil + return sc, nodeInfo, nil } -func (mt *MultiplexTransport) wrapPeer( - c net.Conn, - ni NodeInfo, - cfg peerConfig, - socketAddr *NetAddress, -) Peer { - persistent := false - if cfg.isPersistent != nil { - if cfg.outbound { - persistent = cfg.isPersistent(socketAddr) - } else { - selfReportedAddr := ni.NetAddress - persistent = cfg.isPersistent(selfReportedAddr) - } +// newMultiplexPeer creates a new multiplex Peer, using +// the provided Peer behavior and info +func (mt *MultiplexTransport) newMultiplexPeer( + info peerInfo, + behavior PeerBehavior, + isOutbound bool, +) (Peer, error) { + // Extract the host + host, _, err := net.SplitHostPort(info.conn.RemoteAddr().String()) + if err != nil { + return nil, fmt.Errorf("unable to extract peer host, %w", err) } - peerConn := newPeerConn( - cfg.outbound, - persistent, - c, - socketAddr, - ) + // Look up the IPs + ips, err := net.LookupIP(host) + if err != nil { + return nil, fmt.Errorf("unable to lookup peer IPs, %w", err) + } - p := newPeer( - peerConn, - mt.mConfig, - ni, - cfg.reactorsByCh, - cfg.chDescs, - cfg.onPeerError, - ) + // Wrap the info related to the connection + peerConn := &ConnInfo{ + Outbound: isOutbound, + Persistent: behavior.IsPersistentPeer(info.addr.ID), + Private: behavior.IsPrivatePeer(info.nodeInfo.ID()), + Conn: info.conn, + RemoteIP: ips[0], // IPv4 + SocketAddr: info.addr, + } + + // Create the info related to the multiplex connection + mConfig := &ConnConfig{ + MConfig: mt.mConfig, + ReactorsByCh: behavior.Reactors(), + ChDescs: behavior.ReactorChDescriptors(), + OnPeerError: behavior.HandlePeerError, + } - return p + return newPeer(peerConn, info.nodeInfo, mConfig), nil } -func handshake( - c net.Conn, +// exchangeNodeInfo performs a data swap, where node +// info is exchanged between the current node and a peer async +func exchangeNodeInfo( + c secretConn, timeout time.Duration, - nodeInfo NodeInfo, -) (NodeInfo, error) { + nodeInfo types.NodeInfo, +) (types.NodeInfo, error) { if err := c.SetDeadline(time.Now().Add(timeout)); err != nil { - return NodeInfo{}, err + return types.NodeInfo{}, err } var ( - errc = make(chan error, 2) - - peerNodeInfo NodeInfo + peerNodeInfo types.NodeInfo ourNodeInfo = nodeInfo ) - go func(errc chan<- error, c net.Conn) { + g, _ := errgroup.WithContext(context.Background()) + + g.Go(func() error { _, err := amino.MarshalSizedWriter(c, ourNodeInfo) - errc <- err - }(errc, c) - go func(errc chan<- error, c net.Conn) { + + return err + }) + + g.Go(func() error { _, err := amino.UnmarshalSizedReader( c, &peerNodeInfo, - int64(MaxNodeInfoSize()), + types.MaxNodeInfoSize, ) - errc <- err - }(errc, c) - for i := 0; i < cap(errc); i++ { - err := <-errc - if err != nil { - return NodeInfo{}, err - } + return err + }) + + if err := g.Wait(); err != nil { + return types.NodeInfo{}, err + } + + // Validate the received node information + if err := nodeInfo.Validate(); err != nil { + return types.NodeInfo{}, fmt.Errorf("unable to validate node info, %w", err) } return peerNodeInfo, c.SetDeadline(time.Time{}) } -func upgradeSecretConn( +// upgradeToSecretConn takes an active TCP connection, +// and upgrades it to a verified, handshaked connection through +// the STS protocol +func (mt *MultiplexTransport) upgradeToSecretConn( c net.Conn, timeout time.Duration, privKey crypto.PrivKey, -) (*conn.SecretConnection, error) { +) (secretConn, error) { if err := c.SetDeadline(time.Now().Add(timeout)); err != nil { return nil, err } - sc, err := conn.MakeSecretConnection(c, privKey) + // Handshake (STS) + sc, err := mt.connUpgradeFn(c, privKey) if err != nil { return nil, err } return sc, sc.SetDeadline(time.Time{}) } - -func resolveIPs(resolver IPResolver, c net.Conn) ([]net.IP, error) { - host, _, err := net.SplitHostPort(c.RemoteAddr().String()) - if err != nil { - return nil, err - } - - addrs, err := resolver.LookupIPAddr(context.Background(), host) - if err != nil { - return nil, err - } - - ips := []net.IP{} - - for _, addr := range addrs { - ips = append(ips, addr.IP) - } - - return ips, nil -} diff --git a/tm2/pkg/p2p/transport_test.go b/tm2/pkg/p2p/transport_test.go index 63b1c26e666..6314570d943 100644 --- a/tm2/pkg/p2p/transport_test.go +++ b/tm2/pkg/p2p/transport_test.go @@ -1,650 +1,519 @@ package p2p import ( + "context" "fmt" - "math/rand" "net" - "reflect" "testing" "time" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/p2p/conn" - "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/gnolang/gno/tm2/pkg/versionset" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -var defaultNodeName = "host_peer" - -func emptyNodeInfo() NodeInfo { - return NodeInfo{} -} - -// newMultiplexTransport returns a tcp connected multiplexed peer -// using the default MConnConfig. It's a convenience function used -// for testing. -func newMultiplexTransport( - nodeInfo NodeInfo, - nodeKey NodeKey, -) *MultiplexTransport { - return NewMultiplexTransport( - nodeInfo, nodeKey, conn.DefaultMConnConfig(), - ) -} - -func TestTransportMultiplexConnFilter(t *testing.T) { - t.Parallel() - - mt := newMultiplexTransport( - emptyNodeInfo(), - NodeKey{ - PrivKey: ed25519.GenPrivKey(), - }, - ) - id := mt.nodeKey.ID() - - MultiplexTransportConnFilters( - func(_ ConnSet, _ net.Conn, _ []net.IP) error { return nil }, - func(_ ConnSet, _ net.Conn, _ []net.IP) error { return nil }, - func(_ ConnSet, _ net.Conn, _ []net.IP) error { - return fmt.Errorf("rejected") - }, - )(mt) - - addr, err := NewNetAddressFromString(NetAddressString(id, "127.0.0.1:0")) - if err != nil { - t.Fatal(err) - } - - if err := mt.Listen(*addr); err != nil { - t.Fatal(err) - } +// generateNetAddr generates dummy net addresses +func generateNetAddr(t *testing.T, count int) []*types.NetAddress { + t.Helper() - errc := make(chan error) + addrs := make([]*types.NetAddress, 0, count) - go func() { - addr := NewNetAddress(id, mt.listener.Addr()) + for i := 0; i < count; i++ { + key := types.GenerateNodeKey() - _, err := addr.Dial() - if err != nil { - errc <- err - return - } + // Grab a random port + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) - close(errc) - }() + addr, err := types.NewNetAddress(key.ID(), ln.Addr()) + require.NoError(t, err) - if err := <-errc; err != nil { - t.Errorf("connection failed: %v", err) + addrs = append(addrs, addr) } - _, err = mt.Accept(peerConfig{}) - if err, ok := err.(RejectedError); ok { - if !err.IsFiltered() { - t.Errorf("expected peer to be filtered") - } - } else { - t.Errorf("expected RejectedError") - } + return addrs } -func TestTransportMultiplexConnFilterTimeout(t *testing.T) { +func TestMultiplexTransport_NetAddress(t *testing.T) { t.Parallel() - mt := newMultiplexTransport( - emptyNodeInfo(), - NodeKey{ - PrivKey: ed25519.GenPrivKey(), - }, - ) - id := mt.nodeKey.ID() - - MultiplexTransportFilterTimeout(5 * time.Millisecond)(mt) - MultiplexTransportConnFilters( - func(_ ConnSet, _ net.Conn, _ []net.IP) error { - time.Sleep(100 * time.Millisecond) - return nil - }, - )(mt) - - addr, err := NewNetAddressFromString(NetAddressString(id, "127.0.0.1:0")) - if err != nil { - t.Fatal(err) - } - - if err := mt.Listen(*addr); err != nil { - t.Fatal(err) - } - - errc := make(chan error) + t.Run("transport not active", func(t *testing.T) { + t.Parallel() - go func() { - addr := NewNetAddress(id, mt.listener.Addr()) - - _, err := addr.Dial() - if err != nil { - errc <- err - return - } - - close(errc) - }() - - if err := <-errc; err != nil { - t.Errorf("connection failed: %v", err) - } - - _, err = mt.Accept(peerConfig{}) - if _, ok := err.(FilterTimeoutError); !ok { - t.Errorf("expected FilterTimeoutError") - } -} - -func TestTransportMultiplexAcceptMultiple(t *testing.T) { - t.Parallel() + var ( + ni = types.NodeInfo{} + nk = types.NodeKey{} + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + ) - mt := testSetupMultiplexTransport(t) - laddr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) + transport := NewMultiplexTransport(ni, nk, mCfg, logger) + addr := transport.NetAddress() - var ( - seed = rand.New(rand.NewSource(time.Now().UnixNano())) - nDialers = seed.Intn(64) + 64 - errc = make(chan error, nDialers) - ) + assert.Error(t, addr.Validate()) + }) - // Setup dialers. - for i := 0; i < nDialers; i++ { - go testDialer(*laddr, errc) - } + t.Run("active transport on random port", func(t *testing.T) { + t.Parallel() - // Catch connection errors. - for i := 0; i < nDialers; i++ { - if err := <-errc; err != nil { - t.Fatal(err) - } - } + var ( + ni = types.NodeInfo{} + nk = types.NodeKey{} + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + addr = generateNetAddr(t, 1)[0] + ) - ps := []Peer{} + addr.Port = 0 // random port - // Accept all peers. - for i := 0; i < cap(errc); i++ { - p, err := mt.Accept(peerConfig{}) - if err != nil { - t.Fatal(err) - } + transport := NewMultiplexTransport(ni, nk, mCfg, logger) - if err := p.Start(); err != nil { - t.Fatal(err) - } + require.NoError(t, transport.Listen(*addr)) + defer func() { + require.NoError(t, transport.Close()) + }() - ps = append(ps, p) - } + netAddr := transport.NetAddress() + assert.False(t, netAddr.Equals(*addr)) + assert.NoError(t, netAddr.Validate()) + }) - if have, want := len(ps), cap(errc); have != want { - t.Errorf("have %v, want %v", have, want) - } + t.Run("active transport on specific port", func(t *testing.T) { + t.Parallel() - // Stop all peers. - for _, p := range ps { - if err := p.Stop(); err != nil { - t.Fatal(err) - } - } + var ( + ni = types.NodeInfo{} + nk = types.NodeKey{} + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + addr = generateNetAddr(t, 1)[0] + ) - if err := mt.Close(); err != nil { - t.Errorf("close errored: %v", err) - } -} + addr.Port = 4123 // specific port -func testDialer(dialAddr NetAddress, errc chan error) { - var ( - pv = ed25519.GenPrivKey() - dialer = newMultiplexTransport( - testNodeInfo(pv.PubKey().Address().ID(), defaultNodeName), - NodeKey{ - PrivKey: pv, - }, - ) - ) + transport := NewMultiplexTransport(ni, nk, mCfg, logger) - _, err := dialer.Dial(dialAddr, peerConfig{}) - if err != nil { - errc <- err - return - } + require.NoError(t, transport.Listen(*addr)) + defer func() { + require.NoError(t, transport.Close()) + }() - // Signal that the connection was established. - errc <- nil + netAddr := transport.NetAddress() + assert.True(t, netAddr.Equals(*addr)) + assert.NoError(t, netAddr.Validate()) + }) } -func TestFlappyTransportMultiplexAcceptNonBlocking(t *testing.T) { +func TestMultiplexTransport_Accept(t *testing.T) { t.Parallel() - testutils.FilterStability(t, testutils.Flappy) + t.Run("inactive transport", func(t *testing.T) { + t.Parallel() - mt := testSetupMultiplexTransport(t) - - var ( - fastNodePV = ed25519.GenPrivKey() - fastNodeInfo = testNodeInfo(fastNodePV.PubKey().Address().ID(), "fastnode") - errc = make(chan error) - fastc = make(chan struct{}) - slowc = make(chan struct{}) - ) - - // Simulate slow Peer. - go func() { - addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - - c, err := addr.Dial() - if err != nil { - errc <- err - return - } - - close(slowc) + var ( + ni = types.NodeInfo{} + nk = types.NodeKey{} + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + ) - select { - case <-fastc: - // Fast peer connected. - case <-time.After(100 * time.Millisecond): - // We error if the fast peer didn't succeed. - errc <- fmt.Errorf("Fast peer timed out") - } + transport := NewMultiplexTransport(ni, nk, mCfg, logger) - sc, err := upgradeSecretConn(c, 100*time.Millisecond, ed25519.GenPrivKey()) - if err != nil { - errc <- err - return - } + p, err := transport.Accept(context.Background(), nil) - _, err = handshake(sc, 100*time.Millisecond, - testNodeInfo( - ed25519.GenPrivKey().PubKey().Address().ID(), - "slow_peer", - )) - if err != nil { - errc <- err - return - } - }() + assert.Nil(t, p) + assert.ErrorIs( + t, + err, + errTransportInactive, + ) + }) - // Simulate fast Peer. - go func() { - <-slowc + t.Run("transport closed", func(t *testing.T) { + t.Parallel() - dialer := newMultiplexTransport( - fastNodeInfo, - NodeKey{ - PrivKey: fastNodePV, - }, + var ( + ni = types.NodeInfo{} + nk = types.NodeKey{} + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + addr = generateNetAddr(t, 1)[0] ) - addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err := dialer.Dial(*addr, peerConfig{}) - if err != nil { - errc <- err - return - } + addr.Port = 0 - close(errc) - close(fastc) - }() + transport := NewMultiplexTransport(ni, nk, mCfg, logger) - if err := <-errc; err != nil { - t.Errorf("connection failed: %v", err) - } + // Start the transport + require.NoError(t, transport.Listen(*addr)) - p, err := mt.Accept(peerConfig{}) - if err != nil { - t.Fatal(err) - } + // Stop the transport + require.NoError(t, transport.Close()) - if have, want := p.NodeInfo(), fastNodeInfo; !reflect.DeepEqual(have, want) { - t.Errorf("have %v, want %v", have, want) - } -} - -func TestTransportMultiplexValidateNodeInfo(t *testing.T) { - t.Parallel() + p, err := transport.Accept(context.Background(), nil) - mt := testSetupMultiplexTransport(t) + assert.Nil(t, p) + assert.ErrorIs( + t, + err, + errTransportClosed, + ) + }) - errc := make(chan error) + t.Run("context canceled", func(t *testing.T) { + t.Parallel() - go func() { var ( - pv = ed25519.GenPrivKey() - dialer = newMultiplexTransport( - testNodeInfo(pv.PubKey().Address().ID(), ""), // Should not be empty - NodeKey{ - PrivKey: pv, - }, - ) + ni = types.NodeInfo{} + nk = types.NodeKey{} + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + addr = generateNetAddr(t, 1)[0] ) - addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) + addr.Port = 0 - _, err := dialer.Dial(*addr, peerConfig{}) - if err != nil { - errc <- err - return - } + transport := NewMultiplexTransport(ni, nk, mCfg, logger) - close(errc) - }() + // Start the transport + require.NoError(t, transport.Listen(*addr)) - if err := <-errc; err != nil { - t.Errorf("connection failed: %v", err) - } + ctx, cancelFn := context.WithCancel(context.Background()) + cancelFn() - _, err := mt.Accept(peerConfig{}) - if err, ok := err.(RejectedError); ok { - if !err.IsNodeInfoInvalid() { - t.Errorf("expected NodeInfo to be invalid") - } - } else { - t.Errorf("expected RejectedError") - } -} + p, err := transport.Accept(ctx, nil) -func TestTransportMultiplexRejectMismatchID(t *testing.T) { - t.Parallel() + assert.Nil(t, p) + assert.ErrorIs( + t, + err, + context.Canceled, + ) + }) - mt := testSetupMultiplexTransport(t) + t.Run("peer ID mismatch", func(t *testing.T) { + t.Parallel() - errc := make(chan error) + var ( + network = "dev" + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + keys = []*types.NodeKey{ + types.GenerateNodeKey(), + types.GenerateNodeKey(), + } - go func() { - dialer := newMultiplexTransport( - testNodeInfo( - ed25519.GenPrivKey().PubKey().Address().ID(), "dialer", - ), - NodeKey{ - PrivKey: ed25519.GenPrivKey(), - }, + peerBehavior = &reactorPeerBehavior{ + chDescs: make([]*conn.ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + handlePeerErrFn: func(_ Peer, err error) { + require.NoError(t, err) + }, + isPersistentPeerFn: func(_ types.ID) bool { + return false + }, + isPrivatePeerFn: func(_ types.ID) bool { + return false + }, + } ) - addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err := dialer.Dial(*addr, peerConfig{}) - if err != nil { - errc <- err - return - } + peers := make([]*MultiplexTransport, 0, len(keys)) - close(errc) - }() + for index, key := range keys { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + require.NoError(t, err) - if err := <-errc; err != nil { - t.Errorf("connection failed: %v", err) - } + id := key.ID() - _, err := mt.Accept(peerConfig{}) - if err, ok := err.(RejectedError); ok { - if !err.IsAuthFailure() { - t.Errorf("expected auth failure") - } - } else { - t.Errorf("expected RejectedError") - } -} + if index%1 == 0 { + // Hijack the key value + id = types.GenerateNodeKey().ID() + } -func TestTransportMultiplexDialRejectWrongID(t *testing.T) { - t.Parallel() + na, err := types.NewNetAddress(id, addr) + require.NoError(t, err) - mt := testSetupMultiplexTransport(t) + ni := types.NodeInfo{ + Network: network, // common network + PeerID: id, + Version: "v1.0.0-rc.0", + Moniker: fmt.Sprintf("node-%d", index), + VersionSet: make(versionset.VersionSet, 0), // compatible version set + Channels: []byte{42}, // common channel + } - var ( - pv = ed25519.GenPrivKey() - dialer = newMultiplexTransport( - testNodeInfo(pv.PubKey().Address().ID(), ""), // Should not be empty - NodeKey{ - PrivKey: pv, - }, - ) - ) + // Create a fresh transport + tr := NewMultiplexTransport(ni, *key, mCfg, logger) - wrongID := ed25519.GenPrivKey().PubKey().Address().ID() - addr := NewNetAddress(wrongID, mt.listener.Addr()) + // Start the transport + require.NoError(t, tr.Listen(*na)) - _, err := dialer.Dial(*addr, peerConfig{}) - if err != nil { - t.Logf("connection failed: %v", err) - if err, ok := err.(RejectedError); ok { - if !err.IsAuthFailure() { - t.Errorf("expected auth failure") - } - } else { - t.Errorf("expected RejectedError") + t.Cleanup(func() { + assert.NoError(t, tr.Close()) + }) + + peers = append( + peers, + tr, + ) } - } -} -func TestTransportMultiplexRejectIncompatible(t *testing.T) { - t.Parallel() + // Make peer 1 --dial--> peer 2, and handshake. + // This "upgrade" should fail because the peer shared a different + // peer ID than what they actually used for the secret connection + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - mt := testSetupMultiplexTransport(t) + p, err := peers[0].Dial(ctx, peers[1].netAddr, peerBehavior) + assert.ErrorIs(t, err, errPeerIDNodeInfoMismatch) + require.Nil(t, p) + }) - errc := make(chan error) + t.Run("incompatible peers", func(t *testing.T) { + t.Parallel() - go func() { var ( - pv = ed25519.GenPrivKey() - dialer = newMultiplexTransport( - testNodeInfoWithNetwork(pv.PubKey().Address().ID(), "dialer", "incompatible-network"), - NodeKey{ - PrivKey: pv, + network = "dev" + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + keys = []*types.NodeKey{ + types.GenerateNodeKey(), + types.GenerateNodeKey(), + } + + peerBehavior = &reactorPeerBehavior{ + chDescs: make([]*conn.ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + handlePeerErrFn: func(_ Peer, err error) { + require.NoError(t, err) }, - ) + isPersistentPeerFn: func(_ types.ID) bool { + return false + }, + isPrivatePeerFn: func(_ types.ID) bool { + return false + }, + } ) - addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err := dialer.Dial(*addr, peerConfig{}) - if err != nil { - errc <- err - return - } + peers := make([]*MultiplexTransport, 0, len(keys)) - close(errc) - }() + for index, key := range keys { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + require.NoError(t, err) - _, err := mt.Accept(peerConfig{}) - if err, ok := err.(RejectedError); ok { - if !err.IsIncompatible() { - t.Errorf("expected to reject incompatible") - } - } else { - t.Errorf("expected RejectedError") - } -} + id := key.ID() -func TestTransportMultiplexRejectSelf(t *testing.T) { - t.Parallel() + na, err := types.NewNetAddress(id, addr) + require.NoError(t, err) - mt := testSetupMultiplexTransport(t) + chainID := network - errc := make(chan error) + if index%2 == 0 { + chainID = "totally-random-network" + } - go func() { - addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) + ni := types.NodeInfo{ + Network: chainID, + PeerID: id, + Version: "v1.0.0-rc.0", + Moniker: fmt.Sprintf("node-%d", index), + VersionSet: make(versionset.VersionSet, 0), // compatible version set + Channels: []byte{42}, // common channel + } - _, err := mt.Dial(*addr, peerConfig{}) - if err != nil { - errc <- err - return - } + // Create a fresh transport + tr := NewMultiplexTransport(ni, *key, mCfg, logger) - close(errc) - }() + // Start the transport + require.NoError(t, tr.Listen(*na)) - if err := <-errc; err != nil { - if err, ok := err.(RejectedError); ok { - if !err.IsSelf() { - t.Errorf("expected to reject self, got: %v", err) - } - } else { - t.Errorf("expected RejectedError") - } - } else { - t.Errorf("expected connection failure") - } + t.Cleanup(func() { + assert.NoError(t, tr.Close()) + }) - _, err := mt.Accept(peerConfig{}) - if err, ok := err.(RejectedError); ok { - if !err.IsSelf() { - t.Errorf("expected to reject self, got: %v", err) + peers = append( + peers, + tr, + ) } - } else { - t.Errorf("expected RejectedError") - } -} -func TestTransportConnDuplicateIPFilter(t *testing.T) { - t.Parallel() + // Make peer 1 --dial--> peer 2, and handshake. + // This "upgrade" should fail because the peer shared a different + // peer ID than what they actually used for the secret connection + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - filter := ConnDuplicateIPFilter() + p, err := peers[0].Dial(ctx, peers[1].netAddr, peerBehavior) + assert.ErrorIs(t, err, errIncompatibleNodeInfo) + require.Nil(t, p) + }) - if err := filter(nil, &testTransportConn{}, nil); err != nil { - t.Fatal(err) - } + t.Run("dialed peer ID mismatch", func(t *testing.T) { + t.Parallel() - var ( - c = &testTransportConn{} - cs = NewConnSet() - ) + var ( + network = "dev" + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + keys = []*types.NodeKey{ + types.GenerateNodeKey(), + types.GenerateNodeKey(), + } - cs.Set(c, []net.IP{ - {10, 0, 10, 1}, - {10, 0, 10, 2}, - {10, 0, 10, 3}, - }) + peerBehavior = &reactorPeerBehavior{ + chDescs: make([]*conn.ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + handlePeerErrFn: func(_ Peer, err error) { + require.NoError(t, err) + }, + isPersistentPeerFn: func(_ types.ID) bool { + return false + }, + isPrivatePeerFn: func(_ types.ID) bool { + return false + }, + } + ) - if err := filter(cs, c, []net.IP{ - {10, 0, 10, 2}, - }); err == nil { - t.Errorf("expected Peer to be rejected as duplicate") - } -} + peers := make([]*MultiplexTransport, 0, len(keys)) -func TestTransportHandshake(t *testing.T) { - t.Parallel() + for index, key := range keys { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + require.NoError(t, err) - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } + na, err := types.NewNetAddress(key.ID(), addr) + require.NoError(t, err) - var ( - peerPV = ed25519.GenPrivKey() - peerNodeInfo = testNodeInfo(peerPV.PubKey().Address().ID(), defaultNodeName) - ) + ni := types.NodeInfo{ + Network: network, // common network + PeerID: key.ID(), + Version: "v1.0.0-rc.0", + Moniker: fmt.Sprintf("node-%d", index), + VersionSet: make(versionset.VersionSet, 0), // compatible version set + Channels: []byte{42}, // common channel + } - go func() { - c, err := net.Dial(ln.Addr().Network(), ln.Addr().String()) - if err != nil { - t.Error(err) - return - } + // Create a fresh transport + tr := NewMultiplexTransport(ni, *key, mCfg, logger) - go func(c net.Conn) { - _, err := amino.MarshalSizedWriter(c, peerNodeInfo) - if err != nil { - t.Error(err) - } - }(c) - go func(c net.Conn) { - var ni NodeInfo - - _, err := amino.UnmarshalSizedReader( - c, - &ni, - int64(MaxNodeInfoSize()), + // Start the transport + require.NoError(t, tr.Listen(*na)) + + t.Cleanup(func() { + assert.NoError(t, tr.Close()) + }) + + peers = append( + peers, + tr, ) - if err != nil { - t.Error(err) - } - }(c) - }() + } - c, err := ln.Accept() - if err != nil { - t.Fatal(err) - } + // Make peer 1 --dial--> peer 2, and handshake + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - ni, err := handshake(c, 100*time.Millisecond, emptyNodeInfo()) - if err != nil { - t.Fatal(err) - } + p, err := peers[0].Dial( + ctx, + types.NetAddress{ + ID: types.GenerateNodeKey().ID(), // mismatched ID + IP: peers[1].netAddr.IP, + Port: peers[1].netAddr.Port, + }, + peerBehavior, + ) + assert.ErrorIs(t, err, errPeerIDDialMismatch) + assert.Nil(t, p) + }) - if have, want := ni, peerNodeInfo; !reflect.DeepEqual(have, want) { - t.Errorf("have %v, want %v", have, want) - } -} + t.Run("valid peer accepted", func(t *testing.T) { + t.Parallel() -// create listener -func testSetupMultiplexTransport(t *testing.T) *MultiplexTransport { - t.Helper() + var ( + network = "dev" + mCfg = conn.DefaultMConnConfig() + logger = log.NewNoopLogger() + keys = []*types.NodeKey{ + types.GenerateNodeKey(), + types.GenerateNodeKey(), + } - var ( - pv = ed25519.GenPrivKey() - id = pv.PubKey().Address().ID() - mt = newMultiplexTransport( - testNodeInfo( - id, "transport", - ), - NodeKey{ - PrivKey: pv, - }, + peerBehavior = &reactorPeerBehavior{ + chDescs: make([]*conn.ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + handlePeerErrFn: func(_ Peer, err error) { + require.NoError(t, err) + }, + isPersistentPeerFn: func(_ types.ID) bool { + return false + }, + isPrivatePeerFn: func(_ types.ID) bool { + return false + }, + } ) - ) - addr, err := NewNetAddressFromString(NetAddressString(id, "127.0.0.1:0")) - if err != nil { - t.Fatal(err) - } + peers := make([]*MultiplexTransport, 0, len(keys)) - if err := mt.Listen(*addr); err != nil { - t.Fatal(err) - } + for index, key := range keys { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + require.NoError(t, err) - return mt -} + na, err := types.NewNetAddress(key.ID(), addr) + require.NoError(t, err) -type testTransportAddr struct{} + ni := types.NodeInfo{ + Network: network, // common network + PeerID: key.ID(), + Version: "v1.0.0-rc.0", + Moniker: fmt.Sprintf("node-%d", index), + VersionSet: make(versionset.VersionSet, 0), // compatible version set + Channels: []byte{42}, // common channel + } -func (a *testTransportAddr) Network() string { return "tcp" } -func (a *testTransportAddr) String() string { return "test.local:1234" } + // Create a fresh transport + tr := NewMultiplexTransport(ni, *key, mCfg, logger) -type testTransportConn struct{} + // Start the transport + require.NoError(t, tr.Listen(*na)) -func (c *testTransportConn) Close() error { - return fmt.Errorf("Close() not implemented") -} + t.Cleanup(func() { + assert.NoError(t, tr.Close()) + }) -func (c *testTransportConn) LocalAddr() net.Addr { - return &testTransportAddr{} -} + peers = append( + peers, + tr, + ) + } -func (c *testTransportConn) RemoteAddr() net.Addr { - return &testTransportAddr{} -} + // Make peer 1 --dial--> peer 2, and handshake + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() -func (c *testTransportConn) Read(_ []byte) (int, error) { - return -1, fmt.Errorf("Read() not implemented") -} + p, err := peers[0].Dial(ctx, peers[1].netAddr, peerBehavior) + require.NoError(t, err) + require.NotNil(t, p) -func (c *testTransportConn) SetDeadline(_ time.Time) error { - return fmt.Errorf("SetDeadline() not implemented") -} + // Make sure the new peer info is valid + assert.Equal(t, peers[1].netAddr.ID, p.ID()) -func (c *testTransportConn) SetReadDeadline(_ time.Time) error { - return fmt.Errorf("SetReadDeadline() not implemented") -} + assert.Equal(t, peers[1].nodeInfo.Channels, p.NodeInfo().Channels) + assert.Equal(t, peers[1].nodeInfo.Moniker, p.NodeInfo().Moniker) + assert.Equal(t, peers[1].nodeInfo.Network, p.NodeInfo().Network) -func (c *testTransportConn) SetWriteDeadline(_ time.Time) error { - return fmt.Errorf("SetWriteDeadline() not implemented") -} + // Attempt to dial again, expect the dial to fail + // because the connection is already active + dialedPeer, err := peers[0].Dial(ctx, peers[1].netAddr, peerBehavior) + require.ErrorIs(t, err, errDuplicateConnection) + assert.Nil(t, dialedPeer) -func (c *testTransportConn) Write(_ []byte) (int, error) { - return -1, fmt.Errorf("Write() not implemented") + // Remove the peer + peers[0].Remove(p) + }) } diff --git a/tm2/pkg/p2p/types.go b/tm2/pkg/p2p/types.go index 150325f52bb..418c5095347 100644 --- a/tm2/pkg/p2p/types.go +++ b/tm2/pkg/p2p/types.go @@ -1,10 +1,115 @@ package p2p import ( + "context" + "net" + "github.com/gnolang/gno/tm2/pkg/p2p/conn" + "github.com/gnolang/gno/tm2/pkg/p2p/events" + "github.com/gnolang/gno/tm2/pkg/p2p/types" + "github.com/gnolang/gno/tm2/pkg/service" ) type ( ChannelDescriptor = conn.ChannelDescriptor ConnectionStatus = conn.ConnectionStatus ) + +// Peer is a wrapper for a connected peer +type Peer interface { + service.Service + + FlushStop() + + ID() types.ID // peer's cryptographic ID + RemoteIP() net.IP // remote IP of the connection + RemoteAddr() net.Addr // remote address of the connection + + IsOutbound() bool // did we dial the peer + IsPersistent() bool // do we redial this peer when we disconnect + IsPrivate() bool // do we share the peer + + CloseConn() error // close original connection + + NodeInfo() types.NodeInfo // peer's info + Status() ConnectionStatus + SocketAddr() *types.NetAddress // actual address of the socket + + Send(byte, []byte) bool + TrySend(byte, []byte) bool + + Set(string, any) + Get(string) any +} + +// PeerSet has a (immutable) subset of the methods of PeerSet. +type PeerSet interface { + Add(peer Peer) + Remove(key types.ID) bool + Has(key types.ID) bool + Get(key types.ID) Peer + List() []Peer + + NumInbound() uint64 // returns the number of connected inbound nodes + NumOutbound() uint64 // returns the number of connected outbound nodes +} + +// Transport handles peer dialing and connection acceptance. Additionally, +// it is also responsible for any custom connection mechanisms (like handshaking). +// Peers returned by the transport are considered to be verified and sound +type Transport interface { + // NetAddress returns the Transport's dial address + NetAddress() types.NetAddress + + // Accept returns a newly connected inbound peer + Accept(context.Context, PeerBehavior) (Peer, error) + + // Dial dials a peer, and returns it + Dial(context.Context, types.NetAddress, PeerBehavior) (Peer, error) + + // Remove drops any resources associated + // with the Peer in the transport + Remove(Peer) +} + +// Switch is the abstraction in the p2p module that handles +// and manages peer connections thorough a Transport +type Switch interface { + // Broadcast publishes data on the given channel, to all peers + Broadcast(chID byte, data []byte) + + // Peers returns the latest peer set + Peers() PeerSet + + // Subscribe subscribes to active switch events + Subscribe(filterFn events.EventFilter) (<-chan events.Event, func()) + + // StopPeerForError stops the peer with the given reason + StopPeerForError(peer Peer, err error) + + // DialPeers marks the given peers as ready for async dialing + DialPeers(peerAddrs ...*types.NetAddress) +} + +// PeerBehavior wraps the Reactor and MultiplexSwitch information a Transport would need when +// dialing or accepting new Peer connections. +// It is worth noting that the only reason why this information is required in the first place, +// is because Peers expose an API through which different TM modules can interact with them. +// In the future™, modules should not directly "Send" anything to Peers, but instead communicate through +// other mediums, such as the P2P module +type PeerBehavior interface { + // ReactorChDescriptors returns the Reactor channel descriptors + ReactorChDescriptors() []*conn.ChannelDescriptor + + // Reactors returns the node's active p2p Reactors (modules) + Reactors() map[byte]Reactor + + // HandlePeerError propagates a peer connection error for further processing + HandlePeerError(Peer, error) + + // IsPersistentPeer returns a flag indicating if the given peer is persistent + IsPersistentPeer(types.ID) bool + + // IsPrivatePeer returns a flag indicating if the given peer is private + IsPrivatePeer(types.ID) bool +} diff --git a/tm2/pkg/p2p/types/key.go b/tm2/pkg/p2p/types/key.go new file mode 100644 index 00000000000..bc45de709d8 --- /dev/null +++ b/tm2/pkg/p2p/types/key.go @@ -0,0 +1,113 @@ +package types + +import ( + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +// ID represents the cryptographically unique Peer ID +type ID = crypto.ID + +// NewIDFromStrings returns an array of ID's build using +// the provided strings +func NewIDFromStrings(idStrs []string) ([]ID, []error) { + var ( + ids = make([]ID, 0, len(idStrs)) + errs = make([]error, 0, len(idStrs)) + ) + + for _, idStr := range idStrs { + id := ID(idStr) + if err := id.Validate(); err != nil { + errs = append(errs, err) + + continue + } + + ids = append(ids, id) + } + + return ids, errs +} + +// NodeKey is the persistent peer key. +// It contains the nodes private key for authentication. +// NOTE: keep in sync with gno.land/cmd/gnoland/secrets.go +type NodeKey struct { + crypto.PrivKey `json:"priv_key"` // our priv key +} + +// ID returns the bech32 representation +// of the node's public p2p key, with +// the bech32 prefix +func (k NodeKey) ID() ID { + return k.PubKey().Address().ID() +} + +// LoadOrGenNodeKey attempts to load the NodeKey from the given filePath. +// If the file does not exist, it generates and saves a new NodeKey. +func LoadOrGenNodeKey(path string) (*NodeKey, error) { + // Check if the key exists + if osm.FileExists(path) { + // Load the node key + return LoadNodeKey(path) + } + + // Key is not present on path, + // generate a fresh one + nodeKey := GenerateNodeKey() + if err := saveNodeKey(path, nodeKey); err != nil { + return nil, fmt.Errorf("unable to save node key, %w", err) + } + + return nodeKey, nil +} + +// LoadNodeKey loads the node key from the given path +func LoadNodeKey(path string) (*NodeKey, error) { + // Load the key + jsonBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("unable to read key, %w", err) + } + + var nodeKey NodeKey + + // Parse the key + if err = amino.UnmarshalJSON(jsonBytes, &nodeKey); err != nil { + return nil, fmt.Errorf("unable to JSON unmarshal node key, %w", err) + } + + return &nodeKey, nil +} + +// GenerateNodeKey generates a random +// node P2P key, based on ed25519 +func GenerateNodeKey() *NodeKey { + privKey := ed25519.GenPrivKey() + + return &NodeKey{ + PrivKey: privKey, + } +} + +// saveNodeKey saves the node key +func saveNodeKey(path string, nodeKey *NodeKey) error { + // Get Amino JSON + marshalledData, err := amino.MarshalJSONIndent(nodeKey, "", "\t") + if err != nil { + return fmt.Errorf("unable to marshal node key into JSON, %w", err) + } + + // Save the data to disk + if err := os.WriteFile(path, marshalledData, 0o644); err != nil { + return fmt.Errorf("unable to save node key to path, %w", err) + } + + return nil +} diff --git a/tm2/pkg/p2p/types/key_test.go b/tm2/pkg/p2p/types/key_test.go new file mode 100644 index 00000000000..5dc153b08c0 --- /dev/null +++ b/tm2/pkg/p2p/types/key_test.go @@ -0,0 +1,158 @@ +package types + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateKeys generates random node p2p keys +func generateKeys(t *testing.T, count int) []*NodeKey { + t.Helper() + + keys := make([]*NodeKey, count) + + for i := 0; i < count; i++ { + keys[i] = GenerateNodeKey() + } + + return keys +} + +func TestNodeKey_Generate(t *testing.T) { + t.Parallel() + + keys := generateKeys(t, 10) + + for _, key := range keys { + require.NotNil(t, key) + assert.NotNil(t, key.PrivKey) + + // Make sure all keys are unique + for _, keyInner := range keys { + if key.ID() == keyInner.ID() { + continue + } + + assert.False(t, key.Equals(keyInner)) + } + } +} + +func TestNodeKey_Load(t *testing.T) { + t.Parallel() + + t.Run("non-existing key", func(t *testing.T) { + t.Parallel() + + key, err := LoadNodeKey("definitely valid path") + + require.Nil(t, key) + assert.ErrorIs(t, err, os.ErrNotExist) + }) + + t.Run("invalid key format", func(t *testing.T) { + t.Parallel() + + // Generate a random path + path := fmt.Sprintf("%s/key.json", t.TempDir()) + + type random struct { + field string + } + + data, err := json.Marshal(&random{ + field: "random data", + }) + require.NoError(t, err) + + // Save the invalid data format + require.NoError(t, os.WriteFile(path, data, 0o644)) + + // Load the key, that's invalid + key, err := LoadNodeKey(path) + + require.NoError(t, err) + assert.Nil(t, key.PrivKey) + }) + + t.Run("valid key loaded", func(t *testing.T) { + t.Parallel() + + var ( + path = fmt.Sprintf("%s/key.json", t.TempDir()) + key = GenerateNodeKey() + ) + + // Save the key + require.NoError(t, saveNodeKey(path, key)) + + // Load the key, that's valid + loadedKey, err := LoadNodeKey(path) + require.NoError(t, err) + + assert.True(t, key.PrivKey.Equals(loadedKey.PrivKey)) + assert.Equal(t, key.ID(), loadedKey.ID()) + }) +} + +func TestNodeKey_ID(t *testing.T) { + t.Parallel() + + keys := generateKeys(t, 10) + + for _, key := range keys { + // Make sure the ID is valid + id := key.ID() + require.NotNil(t, id) + + assert.NoError(t, id.Validate()) + } +} + +func TestNodeKey_LoadOrGenNodeKey(t *testing.T) { + t.Parallel() + + t.Run("existing key loaded", func(t *testing.T) { + t.Parallel() + + var ( + path = fmt.Sprintf("%s/key.json", t.TempDir()) + key = GenerateNodeKey() + ) + + // Save the key + require.NoError(t, saveNodeKey(path, key)) + + loadedKey, err := LoadOrGenNodeKey(path) + require.NoError(t, err) + + // Make sure the key was not generated + assert.True(t, key.PrivKey.Equals(loadedKey.PrivKey)) + }) + + t.Run("fresh key generated", func(t *testing.T) { + t.Parallel() + + path := fmt.Sprintf("%s/key.json", t.TempDir()) + + // Make sure there is no key at the path + _, err := os.Stat(path) + require.ErrorIs(t, err, os.ErrNotExist) + + // Generate the fresh key + key, err := LoadOrGenNodeKey(path) + require.NoError(t, err) + + // Load the saved key + loadedKey, err := LoadOrGenNodeKey(path) + require.NoError(t, err) + + // Make sure the keys are the same + assert.True(t, key.PrivKey.Equals(loadedKey.PrivKey)) + }) +} diff --git a/tm2/pkg/p2p/netaddress.go b/tm2/pkg/p2p/types/netaddress.go similarity index 52% rename from tm2/pkg/p2p/netaddress.go rename to tm2/pkg/p2p/types/netaddress.go index 77f89b2a4b3..a43f90454ea 100644 --- a/tm2/pkg/p2p/netaddress.go +++ b/tm2/pkg/p2p/types/netaddress.go @@ -2,143 +2,156 @@ // Originally Copyright (c) 2013-2014 Conformal Systems LLC. // https://github.com/conformal/btcd/blob/master/LICENSE -package p2p +package types import ( - "flag" + "context" "fmt" "net" "strconv" "strings" - "time" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/errors" ) -type ID = crypto.ID +const ( + nilNetAddress = "" + badNetAddress = "" +) + +var ( + ErrInvalidTCPAddress = errors.New("invalid TCP address") + ErrUnsetIPAddress = errors.New("unset IP address") + ErrInvalidIP = errors.New("invalid IP address") + ErrUnspecifiedIP = errors.New("unspecified IP address") + ErrInvalidNetAddress = errors.New("invalid net address") + ErrEmptyHost = errors.New("empty host address") +) // NetAddress defines information about a peer on the network -// including its Address, IP address, and port. -// NOTE: NetAddress is not meant to be mutated due to memoization. -// @amino2: immutable XXX +// including its ID, IP address, and port type NetAddress struct { - ID ID `json:"id"` // authenticated identifier (TODO) - IP net.IP `json:"ip"` // part of "addr" - Port uint16 `json:"port"` // part of "addr" - - // TODO: - // Name string `json:"name"` // optional DNS name - - // memoize .String() - str string + ID ID `json:"id"` // unique peer identifier (public key address) + IP net.IP `json:"ip"` // the IP part of the dial address + Port uint16 `json:"port"` // the port part of the dial address } // NetAddressString returns id@addr. It strips the leading // protocol from protocolHostPort if it exists. func NetAddressString(id ID, protocolHostPort string) string { - addr := removeProtocolIfDefined(protocolHostPort) - return fmt.Sprintf("%s@%s", id, addr) + return fmt.Sprintf( + "%s@%s", + id, + removeProtocolIfDefined(protocolHostPort), + ) } // NewNetAddress returns a new NetAddress using the provided TCP -// address. When testing, other net.Addr (except TCP) will result in -// using 0.0.0.0:0. When normal run, other net.Addr (except TCP) will -// panic. Panics if ID is invalid. -// TODO: socks proxies? -func NewNetAddress(id ID, addr net.Addr) *NetAddress { +// address +func NewNetAddress(id ID, addr net.Addr) (*NetAddress, error) { + // Make sure the address is valid tcpAddr, ok := addr.(*net.TCPAddr) if !ok { - if flag.Lookup("test.v") == nil { // normal run - panic(fmt.Sprintf("Only TCPAddrs are supported. Got: %v", addr)) - } else { // in testing - netAddr := NewNetAddressFromIPPort("", net.IP("0.0.0.0"), 0) - netAddr.ID = id - return netAddr - } + return nil, ErrInvalidTCPAddress } + // Validate the ID if err := id.Validate(); err != nil { - panic(fmt.Sprintf("Invalid ID %v: %v (addr: %v)", id, err, addr)) + return nil, fmt.Errorf("unable to verify ID, %w", err) } - ip := tcpAddr.IP - port := uint16(tcpAddr.Port) - na := NewNetAddressFromIPPort("", ip, port) + na := NewNetAddressFromIPPort( + tcpAddr.IP, + uint16(tcpAddr.Port), + ) + + // Set the ID na.ID = id - return na + + return na, nil } // NewNetAddressFromString returns a new NetAddress using the provided address in // the form of "ID@IP:Port". // Also resolves the host if host is not an IP. -// Errors are of type ErrNetAddressXxx where Xxx is in (NoID, Invalid, Lookup) func NewNetAddressFromString(idaddr string) (*NetAddress, error) { - idaddr = removeProtocolIfDefined(idaddr) - spl := strings.Split(idaddr, "@") + var ( + prunedAddr = removeProtocolIfDefined(idaddr) + spl = strings.Split(prunedAddr, "@") + ) + if len(spl) != 2 { - return nil, NetAddressNoIDError{idaddr} + return nil, ErrInvalidNetAddress } - // get ID - id := crypto.ID(spl[0]) + var ( + id = crypto.ID(spl[0]) + addr = spl[1] + ) + + // Validate the ID if err := id.Validate(); err != nil { - return nil, NetAddressInvalidError{idaddr, err} + return nil, fmt.Errorf("unable to verify address ID, %w", err) } - addr := spl[1] - // get host and port + // Extract the host and port host, portStr, err := net.SplitHostPort(addr) if err != nil { - return nil, NetAddressInvalidError{addr, err} + return nil, fmt.Errorf("unable to split host and port, %w", err) } - if len(host) == 0 { - return nil, NetAddressInvalidError{ - addr, - errors.New("host is empty"), - } + + if host == "" { + return nil, ErrEmptyHost } ip := net.ParseIP(host) if ip == nil { ips, err := net.LookupIP(host) if err != nil { - return nil, NetAddressLookupError{host, err} + return nil, fmt.Errorf("unable to look up IP, %w", err) } + ip = ips[0] } port, err := strconv.ParseUint(portStr, 10, 16) if err != nil { - return nil, NetAddressInvalidError{portStr, err} + return nil, fmt.Errorf("unable to parse port %s, %w", portStr, err) } - na := NewNetAddressFromIPPort("", ip, uint16(port)) + na := NewNetAddressFromIPPort(ip, uint16(port)) na.ID = id + return na, nil } // NewNetAddressFromStrings returns an array of NetAddress'es build using // the provided strings. func NewNetAddressFromStrings(idaddrs []string) ([]*NetAddress, []error) { - netAddrs := make([]*NetAddress, 0) - errs := make([]error, 0) + var ( + netAddrs = make([]*NetAddress, 0, len(idaddrs)) + errs = make([]error, 0, len(idaddrs)) + ) + for _, addr := range idaddrs { netAddr, err := NewNetAddressFromString(addr) if err != nil { errs = append(errs, err) - } else { - netAddrs = append(netAddrs, netAddr) + + continue } + + netAddrs = append(netAddrs, netAddr) } + return netAddrs, errs } // NewNetAddressFromIPPort returns a new NetAddress using the provided IP // and port number. -func NewNetAddressFromIPPort(id ID, ip net.IP, port uint16) *NetAddress { +func NewNetAddressFromIPPort(ip net.IP, port uint16) *NetAddress { return &NetAddress{ - ID: id, IP: ip, Port: port, } @@ -146,88 +159,78 @@ func NewNetAddressFromIPPort(id ID, ip net.IP, port uint16) *NetAddress { // Equals reports whether na and other are the same addresses, // including their ID, IP, and Port. -func (na *NetAddress) Equals(other interface{}) bool { - if o, ok := other.(*NetAddress); ok { - return na.String() == o.String() - } - return false +func (na *NetAddress) Equals(other NetAddress) bool { + return na.String() == other.String() } // Same returns true is na has the same non-empty ID or DialString as other. -func (na *NetAddress) Same(other interface{}) bool { - if o, ok := other.(*NetAddress); ok { - if na.DialString() == o.DialString() { - return true - } - if na.ID != "" && na.ID == o.ID { - return true - } - } - return false +func (na *NetAddress) Same(other NetAddress) bool { + var ( + dialsSame = na.DialString() == other.DialString() + IDsSame = na.ID != "" && na.ID == other.ID + ) + + return dialsSame || IDsSame } // String representation: @: func (na *NetAddress) String() string { if na == nil { - return "" - } - if na.str != "" { - return na.str + return nilNetAddress } + str, err := na.MarshalAmino() if err != nil { - return "" + return badNetAddress } + return str } +// MarshalAmino stringifies a NetAddress. // Needed because (a) IP doesn't encode, and (b) the intend of this type is to // serialize to a string anyways. func (na NetAddress) MarshalAmino() (string, error) { - if na.str == "" { - addrStr := na.DialString() - if na.ID != "" { - addrStr = NetAddressString(na.ID, addrStr) - } - na.str = addrStr + addrStr := na.DialString() + + if na.ID != "" { + return NetAddressString(na.ID, addrStr), nil } - return na.str, nil + + return addrStr, nil } -func (na *NetAddress) UnmarshalAmino(str string) (err error) { - na2, err := NewNetAddressFromString(str) +func (na *NetAddress) UnmarshalAmino(raw string) (err error) { + netAddress, err := NewNetAddressFromString(raw) if err != nil { return err } - *na = *na2 + + *na = *netAddress + return nil } func (na *NetAddress) DialString() string { if na == nil { - return "" + return nilNetAddress } + return net.JoinHostPort( na.IP.String(), strconv.FormatUint(uint64(na.Port), 10), ) } -// Dial calls net.Dial on the address. -func (na *NetAddress) Dial() (net.Conn, error) { - conn, err := net.Dial("tcp", na.DialString()) - if err != nil { - return nil, err - } - return conn, nil -} +// DialContext dials the given NetAddress with a context +func (na *NetAddress) DialContext(ctx context.Context) (net.Conn, error) { + var d net.Dialer -// DialTimeout calls net.DialTimeout on the address. -func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) { - conn, err := net.DialTimeout("tcp", na.DialString(), timeout) + conn, err := d.DialContext(ctx, "tcp", na.DialString()) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to dial address, %w", err) } + return conn, nil } @@ -236,47 +239,45 @@ func (na *NetAddress) Routable() bool { if err := na.Validate(); err != nil { return false } + // TODO(oga) bitcoind doesn't include RFC3849 here, but should we? - return !(na.RFC1918() || na.RFC3927() || na.RFC4862() || - na.RFC4193() || na.RFC4843() || na.Local()) + return !(na.RFC1918() || + na.RFC3927() || + na.RFC4862() || + na.RFC4193() || + na.RFC4843() || + na.Local()) } -func (na *NetAddress) ValidateLocal() error { +// Validate validates the NetAddress params +func (na *NetAddress) Validate() error { + // Validate the ID if err := na.ID.Validate(); err != nil { - return err + return fmt.Errorf("unable to validate ID, %w", err) } + + // Make sure the IP is set if na.IP == nil { - return errors.New("no IP") - } - if len(na.IP) != 4 && len(na.IP) != 16 { - return fmt.Errorf("invalid IP bytes: %v", len(na.IP)) - } - if na.RFC3849() || na.IP.Equal(net.IPv4bcast) { - return errors.New("invalid IP", na.IP.IsUnspecified()) + return ErrUnsetIPAddress } - return nil -} -func (na *NetAddress) Validate() error { - if err := na.ID.Validate(); err != nil { - return err - } - if na.IP == nil { - return errors.New("no IP") + // Make sure the IP is valid + ipLen := len(na.IP) + if ipLen != 4 && ipLen != 16 { + return ErrInvalidIP } - if len(na.IP) != 4 && len(na.IP) != 16 { - return fmt.Errorf("invalid IP bytes: %v", len(na.IP)) + + // Check if the IP is unspecified + if na.IP.IsUnspecified() { + return ErrUnspecifiedIP } - if na.IP.IsUnspecified() || na.RFC3849() || na.IP.Equal(net.IPv4bcast) { - return errors.New("invalid IP", na.IP.IsUnspecified()) + + // Check if the IP conforms to standards, or is a broadcast + if na.RFC3849() || na.IP.Equal(net.IPv4bcast) { + return ErrInvalidIP } - return nil -} -// HasID returns true if the address has an ID. -// NOTE: It does not check whether the ID is valid or not. -func (na *NetAddress) HasID() bool { - return !na.ID.IsZero() + return nil } // Local returns true if it is a local address. @@ -284,56 +285,6 @@ func (na *NetAddress) Local() bool { return na.IP.IsLoopback() || zero4.Contains(na.IP) } -// ReachabilityTo checks whenever o can be reached from na. -func (na *NetAddress) ReachabilityTo(o *NetAddress) int { - const ( - Unreachable = 0 - Default = iota - Teredo - Ipv6_weak - Ipv4 - Ipv6_strong - ) - switch { - case !na.Routable(): - return Unreachable - case na.RFC4380(): - switch { - case !o.Routable(): - return Default - case o.RFC4380(): - return Teredo - case o.IP.To4() != nil: - return Ipv4 - default: // ipv6 - return Ipv6_weak - } - case na.IP.To4() != nil: - if o.Routable() && o.IP.To4() != nil { - return Ipv4 - } - return Default - default: /* ipv6 */ - var tunnelled bool - // Is our v6 is tunnelled? - if o.RFC3964() || o.RFC6052() || o.RFC6145() { - tunnelled = true - } - switch { - case !o.Routable(): - return Default - case o.RFC4380(): - return Teredo - case o.IP.To4() != nil: - return Ipv4 - case tunnelled: - // only prioritise ipv6 if we aren't tunnelling it. - return Ipv6_weak - } - return Ipv6_strong - } -} - // RFC1918: IPv4 Private networks (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12) // RFC3849: IPv6 Documentation address (2001:0DB8::/32) // RFC3927: IPv4 Autoconfig (169.254.0.0/16) @@ -376,9 +327,12 @@ func (na *NetAddress) RFC4862() bool { return rfc4862.Contains(na.IP) } func (na *NetAddress) RFC6052() bool { return rfc6052.Contains(na.IP) } func (na *NetAddress) RFC6145() bool { return rfc6145.Contains(na.IP) } +// removeProtocolIfDefined removes the protocol part of the given address func removeProtocolIfDefined(addr string) string { - if strings.Contains(addr, "://") { - return strings.Split(addr, "://")[1] + if !strings.Contains(addr, "://") { + // No protocol part + return addr } - return addr + + return strings.Split(addr, "://")[1] } diff --git a/tm2/pkg/p2p/types/netaddress_test.go b/tm2/pkg/p2p/types/netaddress_test.go new file mode 100644 index 00000000000..1f8f0229b99 --- /dev/null +++ b/tm2/pkg/p2p/types/netaddress_test.go @@ -0,0 +1,323 @@ +package types + +import ( + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func BenchmarkNetAddress_String(b *testing.B) { + key := GenerateNodeKey() + + na, err := NewNetAddressFromString(NetAddressString(key.ID(), "127.0.0.1:0")) + require.NoError(b, err) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = na.String() + } +} + +func TestNewNetAddress(t *testing.T) { + t.Parallel() + + t.Run("invalid TCP address", func(t *testing.T) { + t.Parallel() + + var ( + key = GenerateNodeKey() + address = "127.0.0.1:8080" + ) + + udpAddr, err := net.ResolveUDPAddr("udp", address) + require.NoError(t, err) + + _, err = NewNetAddress(key.ID(), udpAddr) + require.Error(t, err) + }) + + t.Run("invalid ID", func(t *testing.T) { + t.Parallel() + + var ( + id = "" // zero ID + address = "127.0.0.1:8080" + ) + + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + require.NoError(t, err) + + _, err = NewNetAddress(ID(id), tcpAddr) + require.Error(t, err) + }) + + t.Run("valid net address", func(t *testing.T) { + t.Parallel() + + var ( + key = GenerateNodeKey() + address = "127.0.0.1:8080" + ) + + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + require.NoError(t, err) + + addr, err := NewNetAddress(key.ID(), tcpAddr) + require.NoError(t, err) + + assert.Equal(t, fmt.Sprintf("%s@%s", key.ID(), address), addr.String()) + }) +} + +func TestNewNetAddressFromString(t *testing.T) { + t.Parallel() + + t.Run("valid net address", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + addr string + expected string + }{ + {"no protocol", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"tcp input", "tcp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"udp input", "udp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"no protocol", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"tcp input", "tcp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"udp input", "udp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"correct nodeId w/tcp", "tcp://g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + addr, err := NewNetAddressFromString(testCase.addr) + require.NoError(t, err) + + assert.Equal(t, testCase.expected, addr.String()) + }) + } + }) + + t.Run("invalid net address", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + addr string + }{ + {"no node id and no protocol", "127.0.0.1:8080"}, + {"no node id w/ tcp input", "tcp://127.0.0.1:8080"}, + {"no node id w/ udp input", "udp://127.0.0.1:8080"}, + + {"malformed tcp input", "tcp//g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + {"malformed udp input", "udp//g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080"}, + + {"invalid host", "notahost"}, + {"invalid port", "127.0.0.1:notapath"}, + {"invalid host w/ port", "notahost:8080"}, + {"just a port", "8082"}, + {"non-existent port", "127.0.0:8080000"}, + + {"too short nodeId", "deadbeef@127.0.0.1:8080"}, + {"too short, not hex nodeId", "this-isnot-hex@127.0.0.1:8080"}, + {"not bech32 nodeId", "xxxm6kmam774klwlh4dhmhaatd7al02m0h0hdap9l@127.0.0.1:8080"}, + + {"too short nodeId w/tcp", "tcp://deadbeef@127.0.0.1:8080"}, + {"too short notHex nodeId w/tcp", "tcp://this-isnot-hex@127.0.0.1:8080"}, + {"not bech32 nodeId w/tcp", "tcp://xxxxm6kmam774klwlh4dhmhaatd7al02m0h0hdap9l@127.0.0.1:8080"}, + + {"no node id", "tcp://@127.0.0.1:8080"}, + {"no node id or IP", "tcp://@"}, + {"tcp no host, w/ port", "tcp://:26656"}, + {"empty", ""}, + {"node id delimiter 1", "@"}, + {"node id delimiter 2", " @"}, + {"node id delimiter 3", " @ "}, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + addr, err := NewNetAddressFromString(testCase.addr) + + assert.Nil(t, addr) + assert.Error(t, err) + }) + } + }) +} + +func TestNewNetAddressFromStrings(t *testing.T) { + t.Parallel() + + t.Run("invalid addresses", func(t *testing.T) { + t.Parallel() + + var ( + keys = generateKeys(t, 10) + strs = make([]string, 0, len(keys)) + ) + + for index, key := range keys { + if index%2 != 0 { + strs = append( + strs, + fmt.Sprintf("%s@:8080", key.ID()), // missing host + ) + + continue + } + + strs = append( + strs, + fmt.Sprintf("%s@127.0.0.1:8080", key.ID()), + ) + } + + // Convert the strings + addrs, errs := NewNetAddressFromStrings(strs) + + assert.Len(t, errs, len(keys)/2) + assert.Equal(t, len(keys)/2, len(addrs)) + + for index, addr := range addrs { + assert.Contains(t, addr.String(), keys[index*2].ID()) + } + }) + + t.Run("valid addresses", func(t *testing.T) { + t.Parallel() + + var ( + keys = generateKeys(t, 10) + strs = make([]string, 0, len(keys)) + ) + + for _, key := range keys { + strs = append( + strs, + fmt.Sprintf("%s@127.0.0.1:8080", key.ID()), + ) + } + + // Convert the strings + addrs, errs := NewNetAddressFromStrings(strs) + + assert.Len(t, errs, 0) + assert.Equal(t, len(keys), len(addrs)) + + for index, addr := range addrs { + assert.Contains(t, addr.String(), keys[index].ID()) + } + }) +} + +func TestNewNetAddressFromIPPort(t *testing.T) { + t.Parallel() + + var ( + host = "127.0.0.1" + port = uint16(8080) + ) + + addr := NewNetAddressFromIPPort(net.ParseIP(host), port) + + assert.Equal( + t, + fmt.Sprintf("%s:%d", host, port), + addr.String(), + ) +} + +func TestNetAddress_Local(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + addr string + isLocal bool + }{ + { + "local loopback", + "127.0.0.1:8080", + true, + }, + { + "local loopback, zero", + "0.0.0.0:8080", + true, + }, + { + "non-local address", + "200.100.200.100:8080", + false, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + key := GenerateNodeKey() + + addr, err := NewNetAddressFromString( + fmt.Sprintf( + "%s@%s", + key.ID(), + testCase.addr, + ), + ) + require.NoError(t, err) + + assert.Equal(t, testCase.isLocal, addr.Local()) + }) + } +} + +func TestNetAddress_Routable(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + addr string + isRoutable bool + }{ + { + "local loopback", + "127.0.0.1:8080", + false, + }, + { + "routable address", + "gno.land:80", + true, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + key := GenerateNodeKey() + + addr, err := NewNetAddressFromString( + fmt.Sprintf( + "%s@%s", + key.ID(), + testCase.addr, + ), + ) + require.NoError(t, err) + + assert.Equal(t, testCase.isRoutable, addr.Routable()) + }) + } +} diff --git a/tm2/pkg/p2p/types/node_info.go b/tm2/pkg/p2p/types/node_info.go new file mode 100644 index 00000000000..8452cb43cb8 --- /dev/null +++ b/tm2/pkg/p2p/types/node_info.go @@ -0,0 +1,141 @@ +package types + +import ( + "errors" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/strings" + "github.com/gnolang/gno/tm2/pkg/versionset" +) + +const ( + MaxNodeInfoSize = int64(10240) // 10KB + maxNumChannels = 16 // plenty of room for upgrades, for now +) + +var ( + ErrInvalidPeerID = errors.New("invalid peer ID") + ErrInvalidVersion = errors.New("invalid node version") + ErrInvalidMoniker = errors.New("invalid node moniker") + ErrInvalidRPCAddress = errors.New("invalid node RPC address") + ErrExcessiveChannels = errors.New("excessive node channels") + ErrDuplicateChannels = errors.New("duplicate node channels") + ErrIncompatibleNetworks = errors.New("incompatible networks") + ErrNoCommonChannels = errors.New("no common channels") +) + +// NodeInfo is the basic node information exchanged +// between two peers during the Tendermint P2P handshake. +type NodeInfo struct { + // Set of protocol versions + VersionSet versionset.VersionSet `json:"version_set"` + + // Unique peer identifier + PeerID ID `json:"id"` + + // Check compatibility. + // Channels are HexBytes so easier to read as JSON + Network string `json:"network"` // network/chain ID + Software string `json:"software"` // name of immediate software + Version string `json:"version"` // software major.minor.revision + Channels []byte `json:"channels"` // channels this node knows about + + // ASCIIText fields + Moniker string `json:"moniker"` // arbitrary moniker + Other NodeInfoOther `json:"other"` // other application specific data +} + +// NodeInfoOther is the misc. application specific data +type NodeInfoOther struct { + TxIndex string `json:"tx_index"` + RPCAddress string `json:"rpc_address"` +} + +// Validate checks the self-reported NodeInfo is safe. +// It returns an error if there +// are too many Channels, if there are any duplicate Channels, +// if the ListenAddr is malformed, or if the ListenAddr is a host name +// that can not be resolved to some IP +func (info NodeInfo) Validate() error { + // Validate the ID + if err := info.PeerID.Validate(); err != nil { + return fmt.Errorf("%w, %w", ErrInvalidPeerID, err) + } + + // Validate Version + if len(info.Version) > 0 && + (!strings.IsASCIIText(info.Version) || + strings.ASCIITrim(info.Version) == "") { + return ErrInvalidVersion + } + + // Validate Channels - ensure max and check for duplicates. + if len(info.Channels) > maxNumChannels { + return ErrExcessiveChannels + } + + channelMap := make(map[byte]struct{}, len(info.Channels)) + for _, ch := range info.Channels { + if _, ok := channelMap[ch]; ok { + return ErrDuplicateChannels + } + + // Mark the channel as present + channelMap[ch] = struct{}{} + } + + // Validate Moniker. + if !strings.IsASCIIText(info.Moniker) || strings.ASCIITrim(info.Moniker) == "" { + return ErrInvalidMoniker + } + + // XXX: Should we be more strict about address formats? + rpcAddr := info.Other.RPCAddress + if len(rpcAddr) > 0 && (!strings.IsASCIIText(rpcAddr) || strings.ASCIITrim(rpcAddr) == "") { + return ErrInvalidRPCAddress + } + + return nil +} + +// ID returns the local node ID +func (info NodeInfo) ID() ID { + return info.PeerID +} + +// CompatibleWith checks if two NodeInfo are compatible with each other. +// CONTRACT: two nodes are compatible if the Block version and networks match, +// and they have at least one channel in common +func (info NodeInfo) CompatibleWith(other NodeInfo) error { + // Validate the protocol versions + if _, err := info.VersionSet.CompatibleWith(other.VersionSet); err != nil { + return fmt.Errorf("incompatible version sets, %w", err) + } + + // Make sure nodes are on the same network + if info.Network != other.Network { + return ErrIncompatibleNetworks + } + + // Make sure there is at least 1 channel in common + commonFound := false + for _, ch1 := range info.Channels { + for _, ch2 := range other.Channels { + if ch1 == ch2 { + commonFound = true + + break + } + } + + if commonFound { + break + } + } + + if !commonFound { + return ErrNoCommonChannels + } + + return nil +} diff --git a/tm2/pkg/p2p/types/node_info_test.go b/tm2/pkg/p2p/types/node_info_test.go new file mode 100644 index 00000000000..d03d77e608f --- /dev/null +++ b/tm2/pkg/p2p/types/node_info_test.go @@ -0,0 +1,321 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/tm2/pkg/versionset" + "github.com/stretchr/testify/assert" +) + +func TestNodeInfo_Validate(t *testing.T) { + t.Parallel() + + t.Run("invalid peer ID", func(t *testing.T) { + t.Parallel() + + info := &NodeInfo{ + PeerID: "", // zero + } + + assert.ErrorIs(t, info.Validate(), ErrInvalidPeerID) + }) + + t.Run("invalid version", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + version string + }{ + { + "non-ascii version", + "¢§µ", + }, + { + "empty tab version", + fmt.Sprintf("\t"), + }, + { + "empty space version", + fmt.Sprintf(" "), + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + info := &NodeInfo{ + PeerID: GenerateNodeKey().ID(), + Version: testCase.version, + } + + assert.ErrorIs(t, info.Validate(), ErrInvalidVersion) + }) + } + }) + + t.Run("invalid moniker", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + moniker string + }{ + { + "empty moniker", + "", + }, + { + "non-ascii moniker", + "¢§µ", + }, + { + "empty tab moniker", + fmt.Sprintf("\t"), + }, + { + "empty space moniker", + fmt.Sprintf(" "), + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + info := &NodeInfo{ + PeerID: GenerateNodeKey().ID(), + Moniker: testCase.moniker, + } + + assert.ErrorIs(t, info.Validate(), ErrInvalidMoniker) + }) + } + }) + + t.Run("invalid RPC Address", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + rpcAddress string + }{ + { + "non-ascii moniker", + "¢§µ", + }, + { + "empty tab RPC address", + fmt.Sprintf("\t"), + }, + { + "empty space RPC address", + fmt.Sprintf(" "), + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + info := &NodeInfo{ + PeerID: GenerateNodeKey().ID(), + Moniker: "valid moniker", + Other: NodeInfoOther{ + RPCAddress: testCase.rpcAddress, + }, + } + + assert.ErrorIs(t, info.Validate(), ErrInvalidRPCAddress) + }) + } + }) + + t.Run("invalid channels", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + channels []byte + expectedErr error + }{ + { + "too many channels", + make([]byte, maxNumChannels+1), + ErrExcessiveChannels, + }, + { + "duplicate channels", + []byte{ + byte(10), + byte(20), + byte(10), + }, + ErrDuplicateChannels, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + info := &NodeInfo{ + PeerID: GenerateNodeKey().ID(), + Moniker: "valid moniker", + Channels: testCase.channels, + } + + assert.ErrorIs(t, info.Validate(), testCase.expectedErr) + }) + } + }) + + t.Run("valid node info", func(t *testing.T) { + t.Parallel() + + info := &NodeInfo{ + PeerID: GenerateNodeKey().ID(), + Moniker: "valid moniker", + Channels: []byte{10, 20, 30}, + Other: NodeInfoOther{ + RPCAddress: "0.0.0.0:26657", + }, + } + + assert.NoError(t, info.Validate()) + }) +} + +func TestNodeInfo_CompatibleWith(t *testing.T) { + t.Parallel() + + t.Run("incompatible version sets", func(t *testing.T) { + t.Parallel() + + var ( + name = "Block" + + infoOne = &NodeInfo{ + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: "badversion", + }, + }, + } + + infoTwo = &NodeInfo{ + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: "v0.0.0", + }, + }, + } + ) + + assert.Error(t, infoTwo.CompatibleWith(*infoOne)) + }) + + t.Run("incompatible networks", func(t *testing.T) { + t.Parallel() + + var ( + name = "Block" + version = "v0.0.0" + + infoOne = &NodeInfo{ + Network: "+wrong", + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: version, + }, + }, + } + + infoTwo = &NodeInfo{ + Network: "gno", + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: version, + }, + }, + } + ) + + assert.ErrorIs(t, infoTwo.CompatibleWith(*infoOne), ErrIncompatibleNetworks) + }) + + t.Run("no common channels", func(t *testing.T) { + t.Parallel() + + var ( + name = "Block" + version = "v0.0.0" + network = "gno" + + infoOne = &NodeInfo{ + Network: network, + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: version, + }, + }, + Channels: []byte{10}, + } + + infoTwo = &NodeInfo{ + Network: network, + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: version, + }, + }, + Channels: []byte{20}, + } + ) + + assert.ErrorIs(t, infoTwo.CompatibleWith(*infoOne), ErrNoCommonChannels) + }) + + t.Run("fully compatible node infos", func(t *testing.T) { + t.Parallel() + + var ( + name = "Block" + version = "v0.0.0" + network = "gno" + channels = []byte{10, 20, 30} + + infoOne = &NodeInfo{ + Network: network, + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: version, + }, + }, + Channels: channels, + } + + infoTwo = &NodeInfo{ + Network: network, + VersionSet: []versionset.VersionInfo{ + { + Name: name, + Version: version, + }, + }, + Channels: channels[1:], + } + ) + + assert.NoError(t, infoTwo.CompatibleWith(*infoOne)) + }) +} diff --git a/tm2/pkg/p2p/upnp/probe.go b/tm2/pkg/p2p/upnp/probe.go deleted file mode 100644 index 29480e7cecc..00000000000 --- a/tm2/pkg/p2p/upnp/probe.go +++ /dev/null @@ -1,110 +0,0 @@ -package upnp - -import ( - "fmt" - "log/slog" - "net" - "time" -) - -type UPNPCapabilities struct { - PortMapping bool - Hairpin bool -} - -func makeUPNPListener(intPort int, extPort int, logger *slog.Logger) (NAT, net.Listener, net.IP, error) { - nat, err := Discover() - if err != nil { - return nil, nil, nil, fmt.Errorf("NAT upnp could not be discovered: %w", err) - } - logger.Info(fmt.Sprintf("ourIP: %v", nat.(*upnpNAT).ourIP)) - - ext, err := nat.GetExternalAddress() - if err != nil { - return nat, nil, nil, fmt.Errorf("external address error: %w", err) - } - logger.Info(fmt.Sprintf("External address: %v", ext)) - - port, err := nat.AddPortMapping("tcp", extPort, intPort, "Tendermint UPnP Probe", 0) - if err != nil { - return nat, nil, ext, fmt.Errorf("port mapping error: %w", err) - } - logger.Info(fmt.Sprintf("Port mapping mapped: %v", port)) - - // also run the listener, open for all remote addresses. - listener, err := net.Listen("tcp", fmt.Sprintf(":%v", intPort)) - if err != nil { - return nat, nil, ext, fmt.Errorf("error establishing listener: %w", err) - } - return nat, listener, ext, nil -} - -func testHairpin(listener net.Listener, extAddr string, logger *slog.Logger) (supportsHairpin bool) { - // Listener - go func() { - inConn, err := listener.Accept() - if err != nil { - logger.Info(fmt.Sprintf("Listener.Accept() error: %v", err)) - return - } - logger.Info(fmt.Sprintf("Accepted incoming connection: %v -> %v", inConn.LocalAddr(), inConn.RemoteAddr())) - buf := make([]byte, 1024) - n, err := inConn.Read(buf) - if err != nil { - logger.Info(fmt.Sprintf("Incoming connection read error: %v", err)) - return - } - logger.Info(fmt.Sprintf("Incoming connection read %v bytes: %X", n, buf)) - if string(buf) == "test data" { - supportsHairpin = true - return - } - }() - - // Establish outgoing - outConn, err := net.Dial("tcp", extAddr) - if err != nil { - logger.Info(fmt.Sprintf("Outgoing connection dial error: %v", err)) - return - } - - n, err := outConn.Write([]byte("test data")) - if err != nil { - logger.Info(fmt.Sprintf("Outgoing connection write error: %v", err)) - return - } - logger.Info(fmt.Sprintf("Outgoing connection wrote %v bytes", n)) - - // Wait for data receipt - time.Sleep(1 * time.Second) - return supportsHairpin -} - -func Probe(logger *slog.Logger) (caps UPNPCapabilities, err error) { - logger.Info("Probing for UPnP!") - - intPort, extPort := 8001, 8001 - - nat, listener, ext, err := makeUPNPListener(intPort, extPort, logger) - if err != nil { - return - } - caps.PortMapping = true - - // Deferred cleanup - defer func() { - if err := nat.DeletePortMapping("tcp", intPort, extPort); err != nil { - logger.Error(fmt.Sprintf("Port mapping delete error: %v", err)) - } - if err := listener.Close(); err != nil { - logger.Error(fmt.Sprintf("Listener closing error: %v", err)) - } - }() - - supportsHairpin := testHairpin(listener, fmt.Sprintf("%v:%v", ext, extPort), logger) - if supportsHairpin { - caps.Hairpin = true - } - - return -} diff --git a/tm2/pkg/p2p/upnp/upnp.go b/tm2/pkg/p2p/upnp/upnp.go deleted file mode 100644 index cd47ac35553..00000000000 --- a/tm2/pkg/p2p/upnp/upnp.go +++ /dev/null @@ -1,392 +0,0 @@ -// Taken from taipei-torrent. -// Just enough UPnP to be able to forward ports -// For more information, see: http://www.upnp-hacks.org/upnp.html -package upnp - -// TODO: use syscalls to get actual ourIP, see issue #712 - -import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "io" - "net" - "net/http" - "strconv" - "strings" - "time" -) - -type upnpNAT struct { - serviceURL string - ourIP string - urnDomain string -} - -// protocol is either "udp" or "tcp" -type NAT interface { - GetExternalAddress() (addr net.IP, err error) - AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) - DeletePortMapping(protocol string, externalPort, internalPort int) (err error) -} - -func Discover() (nat NAT, err error) { - ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") - if err != nil { - return - } - conn, err := net.ListenPacket("udp4", ":0") - if err != nil { - return - } - socket := conn.(*net.UDPConn) - defer socket.Close() //nolint: errcheck - - if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil { - return nil, err - } - - st := "InternetGatewayDevice:1" - - buf := bytes.NewBufferString( - "M-SEARCH * HTTP/1.1\r\n" + - "HOST: 239.255.255.250:1900\r\n" + - "ST: ssdp:all\r\n" + - "MAN: \"ssdp:discover\"\r\n" + - "MX: 2\r\n\r\n") - message := buf.Bytes() - answerBytes := make([]byte, 1024) - for i := 0; i < 3; i++ { - _, err = socket.WriteToUDP(message, ssdp) - if err != nil { - return - } - var n int - _, _, err = socket.ReadFromUDP(answerBytes) - if err != nil { - return - } - for { - n, _, err = socket.ReadFromUDP(answerBytes) - if err != nil { - break - } - answer := string(answerBytes[0:n]) - if !strings.Contains(answer, st) { - continue - } - // HTTP header field names are case-insensitive. - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - locString := "\r\nlocation:" - answer = strings.ToLower(answer) - locIndex := strings.Index(answer, locString) - if locIndex < 0 { - continue - } - loc := answer[locIndex+len(locString):] - endIndex := strings.Index(loc, "\r\n") - if endIndex < 0 { - continue - } - locURL := strings.TrimSpace(loc[0:endIndex]) - var serviceURL, urnDomain string - serviceURL, urnDomain, err = getServiceURL(locURL) - if err != nil { - return - } - var ourIP net.IP - ourIP, err = localIPv4() - if err != nil { - return - } - nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain} - return - } - } - err = errors.New("UPnP port discovery failed") - return nat, err -} - -type Envelope struct { - XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` - Soap *SoapBody -} - -type SoapBody struct { - XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` - ExternalIP *ExternalIPAddressResponse -} - -type ExternalIPAddressResponse struct { - XMLName xml.Name `xml:"GetExternalIPAddressResponse"` - IPAddress string `xml:"NewExternalIPAddress"` -} - -type ExternalIPAddress struct { - XMLName xml.Name `xml:"NewExternalIPAddress"` - IP string -} - -type UPNPService struct { - ServiceType string `xml:"serviceType"` - ControlURL string `xml:"controlURL"` -} - -type DeviceList struct { - Device []Device `xml:"device"` -} - -type ServiceList struct { - Service []UPNPService `xml:"service"` -} - -type Device struct { - XMLName xml.Name `xml:"device"` - DeviceType string `xml:"deviceType"` - DeviceList DeviceList `xml:"deviceList"` - ServiceList ServiceList `xml:"serviceList"` -} - -type Root struct { - Device Device -} - -func getChildDevice(d *Device, deviceType string) *Device { - dl := d.DeviceList.Device - for i := 0; i < len(dl); i++ { - if strings.Contains(dl[i].DeviceType, deviceType) { - return &dl[i] - } - } - return nil -} - -func getChildService(d *Device, serviceType string) *UPNPService { - sl := d.ServiceList.Service - for i := 0; i < len(sl); i++ { - if strings.Contains(sl[i].ServiceType, serviceType) { - return &sl[i] - } - } - return nil -} - -func localIPv4() (net.IP, error) { - tt, err := net.Interfaces() - if err != nil { - return nil, err - } - for _, t := range tt { - aa, err := t.Addrs() - if err != nil { - return nil, err - } - for _, a := range aa { - ipnet, ok := a.(*net.IPNet) - if !ok { - continue - } - v4 := ipnet.IP.To4() - if v4 == nil || v4[0] == 127 { // loopback address - continue - } - return v4, nil - } - } - return nil, errors.New("cannot find local IP address") -} - -func getServiceURL(rootURL string) (url, urnDomain string, err error) { - r, err := http.Get(rootURL) //nolint: gosec - if err != nil { - return - } - defer r.Body.Close() //nolint: errcheck - - if r.StatusCode >= 400 { - err = errors.New(fmt.Sprint(r.StatusCode)) - return - } - var root Root - err = xml.NewDecoder(r.Body).Decode(&root) - if err != nil { - return - } - a := &root.Device - if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") { - err = errors.New("no InternetGatewayDevice") - return - } - b := getChildDevice(a, "WANDevice:1") - if b == nil { - err = errors.New("no WANDevice") - return - } - c := getChildDevice(b, "WANConnectionDevice:1") - if c == nil { - err = errors.New("no WANConnectionDevice") - return - } - d := getChildService(c, "WANIPConnection:1") - if d == nil { - // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice, - // instead of under WanConnectionDevice - d = getChildService(b, "WANIPConnection:1") - - if d == nil { - err = errors.New("no WANIPConnection") - return - } - } - // Extract the domain name, which isn't always 'schemas-upnp-org' - urnDomain = strings.Split(d.ServiceType, ":")[1] - url = combineURL(rootURL, d.ControlURL) - return url, urnDomain, err -} - -func combineURL(rootURL, subURL string) string { - protocolEnd := "://" - protoEndIndex := strings.Index(rootURL, protocolEnd) - a := rootURL[protoEndIndex+len(protocolEnd):] - rootIndex := strings.Index(a, "/") - return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL -} - -func soapRequest(url, function, message, domain string) (r *http.Response, err error) { - fullMessage := "" + - "\r\n" + - "" + message + "" - - req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") - req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") - // req.Header.Set("Transfer-Encoding", "chunked") - req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"") - req.Header.Set("Connection", "Close") - req.Header.Set("Cache-Control", "no-cache") - req.Header.Set("Pragma", "no-cache") - - // log.Stderr("soapRequest ", req) - - r, err = http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - /*if r.Body != nil { - defer r.Body.Close() - }*/ - - if r.StatusCode >= 400 { - // log.Stderr(function, r.StatusCode) - err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) - r = nil - return - } - return r, err -} - -type statusInfo struct { - externalIPAddress string -} - -func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) { - message := "\r\n" + - "" - - var response *http.Response - response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) - if response != nil { - defer response.Body.Close() //nolint: errcheck - } - if err != nil { - return - } - var envelope Envelope - data, err := io.ReadAll(response.Body) - if err != nil { - return - } - reader := bytes.NewReader(data) - err = xml.NewDecoder(reader).Decode(&envelope) - if err != nil { - return - } - - info = statusInfo{envelope.Soap.ExternalIP.IPAddress} - - if err != nil { - return - } - - return info, err -} - -// GetExternalAddress returns an external IP. If GetExternalIPAddress action -// fails or IP returned is invalid, GetExternalAddress returns an error. -func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { - info, err := n.getExternalIPAddress() - if err != nil { - return - } - addr = net.ParseIP(info.externalIPAddress) - if addr == nil { - err = fmt.Errorf("failed to parse IP: %v", info.externalIPAddress) - } - return -} - -func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { - // A single concatenation would break ARM compilation. - message := "\r\n" + - "" + strconv.Itoa(externalPort) - message += "" + protocol + "" - message += "" + strconv.Itoa(internalPort) + "" + - "" + n.ourIP + "" + - "1" - message += description + - "" + strconv.Itoa(timeout) + - "" - - var response *http.Response - response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) - if response != nil { - defer response.Body.Close() //nolint: errcheck - } - if err != nil { - return - } - - // TODO: check response to see if the port was forwarded - // log.Println(message, response) - // JAE: - // body, err := io.ReadAll(response.Body) - // fmt.Println(string(body), err) - mappedExternalPort = externalPort - _ = response - return -} - -func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { - message := "\r\n" + - "" + strconv.Itoa(externalPort) + - "" + protocol + "" + - "" - - var response *http.Response - response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) - if response != nil { - defer response.Body.Close() //nolint: errcheck - } - if err != nil { - return - } - - // TODO: check response to see if the port was deleted - // log.Println(message, response) - _ = response - return -} diff --git a/tm2/pkg/telemetry/metrics/metrics.go b/tm2/pkg/telemetry/metrics/metrics.go index 7a3e182e06d..7488420baf4 100644 --- a/tm2/pkg/telemetry/metrics/metrics.go +++ b/tm2/pkg/telemetry/metrics/metrics.go @@ -16,12 +16,10 @@ import ( ) const ( - broadcastTxTimerKey = "broadcast_tx_hist" - buildBlockTimerKey = "build_block_hist" + buildBlockTimerKey = "build_block_hist" inboundPeersKey = "inbound_peers_gauge" outboundPeersKey = "outbound_peers_gauge" - dialingPeersKey = "dialing_peers_gauge" numMempoolTxsKey = "num_mempool_txs_hist" numCachedTxsKey = "num_cached_txs_hist" @@ -41,11 +39,6 @@ const ( ) var ( - // Misc // - - // BroadcastTxTimer measures the transaction broadcast duration - BroadcastTxTimer metric.Int64Histogram - // Networking // // InboundPeers measures the active number of inbound peers @@ -54,9 +47,6 @@ var ( // OutboundPeers measures the active number of outbound peers OutboundPeers metric.Int64Gauge - // DialingPeers measures the active number of peers in the dialing state - DialingPeers metric.Int64Gauge - // Mempool // // NumMempoolTxs measures the number of transaction inside the mempool @@ -152,14 +142,6 @@ func Init(config config.Config) error { otel.SetMeterProvider(provider) meter := provider.Meter(config.MeterName) - if BroadcastTxTimer, err = meter.Int64Histogram( - broadcastTxTimerKey, - metric.WithDescription("broadcast tx duration"), - metric.WithUnit("ms"), - ); err != nil { - return fmt.Errorf("unable to create histogram, %w", err) - } - if BuildBlockTimer, err = meter.Int64Histogram( buildBlockTimerKey, metric.WithDescription("block build duration"), @@ -184,18 +166,10 @@ func Init(config config.Config) error { ); err != nil { return fmt.Errorf("unable to create histogram, %w", err) } + // Initialize OutboundPeers Gauge OutboundPeers.Record(ctx, 0) - if DialingPeers, err = meter.Int64Gauge( - dialingPeersKey, - metric.WithDescription("dialing peer count"), - ); err != nil { - return fmt.Errorf("unable to create histogram, %w", err) - } - // Initialize DialingPeers Gauge - DialingPeers.Record(ctx, 0) - // Mempool // if NumMempoolTxs, err = meter.Int64Histogram( numMempoolTxsKey,