diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a98ea413b75..c6e462803b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#9837](https://github.com/cosmos/cosmos-sdk/issues/9837) `--generate-only` flag will accept the keyname now. * [\#10326](https://github.com/cosmos/cosmos-sdk/pull/10326) `x/authz` add query all grants by granter query. * [\#10348](https://github.com/cosmos/cosmos-sdk/pull/10348) Add `fee.{payer,granter}` and `tip` fields to StdSignDoc for signing tipped transactions. +* [\#10379](https://github.com/cosmos/cosmos-sdk/pull/10379) Add validation to `x/upgrade` CLI `software-upgrade` command `--plan-info` value. ### Improvements diff --git a/go.mod b/go.mod index 33f80e91cb69..879d8cd207f6 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 + github.com/hashicorp/go-cleanhttp v0.5.1 // indirect + github.com/hashicorp/go-getter v1.4.1 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87 github.com/improbable-eng/grpc-web v0.15.0 @@ -56,10 +58,14 @@ require ( ) require ( + cloud.google.com/go v0.93.3 // indirect + cloud.google.com/go/storage v1.10.0 // indirect filippo.io/edwards25519 v1.0.0-beta.2 // indirect github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect github.com/Workiva/go-datastructures v1.0.52 // indirect + github.com/aws/aws-sdk-go v1.27.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect @@ -78,16 +84,21 @@ require ( github.com/go-logfmt/logfmt v0.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.0 // indirect github.com/google/orderedcode v0.0.1 // indirect + github.com/googleapis/gax-go/v2 v2.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect github.com/hashicorp/go-immutable-radix v1.0.0 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect github.com/klauspost/compress v1.12.3 // indirect @@ -96,6 +107,8 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect github.com/minio/highwayhash v1.0.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/pelletier/go-toml v1.9.4 // indirect @@ -111,12 +124,17 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect + github.com/ulikunitz/xz v0.5.5 // indirect github.com/zondax/hid v0.9.0 // indirect go.etcd.io/bbolt v1.3.5 // indirect + go.opencensus.io v0.23.0 // indirect golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect golang.org/x/text v0.3.6 // indirect + google.golang.org/api v0.56.0 // indirect + google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index 797136585b6d..85049955ef37 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,7 @@ cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAV cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3 h1:wPBktZFzYBcCZVARvwVKqH1uEj+aLXofJEtrb4oOsio= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -44,6 +45,7 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -115,6 +117,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= +github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= @@ -130,6 +134,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= @@ -170,6 +176,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -359,6 +366,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -416,9 +424,11 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= @@ -443,6 +453,7 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0 h1:6DWmvNpomjL1+3liNSZbVns3zsYzzCjm6pRBO1tLeso= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= @@ -489,7 +500,10 @@ github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOj github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-getter v1.4.1 h1:3A2Mh8smGFcf5M+gmcv898mZdrxpseik45IpcyISLsA= +github.com/hashicorp/go-getter v1.4.1/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -499,11 +513,15 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+ github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -551,8 +569,11 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.10.1 h1:iH+UZfsbRE6vpyZH7asAjTPWJf7RJbpZ9j/N3lDlKs0= github.com/jhump/protoreflect v1.10.1/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= @@ -643,6 +664,7 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= @@ -657,7 +679,9 @@ github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -943,6 +967,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= +github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= @@ -982,6 +1008,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1130,6 +1157,7 @@ golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1357,6 +1385,7 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.56.0 h1:08F9XVYTLOGeSQb3xI9C0gXMuQanhdGed0cWFhDozbI= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1364,6 +1393,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -1455,6 +1485,7 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index 3d793cbab693..fbeeb3d9ee5b 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -1,6 +1,9 @@ package cli import ( + "os" + "path/filepath" + "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" @@ -8,12 +11,15 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov/client/cli" gov "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/upgrade/plan" "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) const ( FlagUpgradeHeight = "upgrade-height" FlagUpgradeInfo = "upgrade-info" + FlagNoValidate = "no-validate" + FlagDaemonName = "daemon-name" ) // GetTxCmd returns the transaction commands for this module @@ -45,6 +51,24 @@ func NewCmdSubmitUpgradeProposal() *cobra.Command { if err != nil { return err } + noValidate, err := cmd.Flags().GetBool(FlagNoValidate) + if err != nil { + return err + } + if !noValidate { + prop := content.(*types.SoftwareUpgradeProposal) + var daemonName string + if daemonName, err = cmd.Flags().GetString(FlagDaemonName); err != nil { + return err + } + var planInfo *plan.Info + if planInfo, err = plan.ParseInfo(prop.Plan.Info); err != nil { + return err + } + if err = planInfo.ValidateFull(daemonName); err != nil { + return err + } + } from := clientCtx.GetFromAddress() @@ -70,7 +94,9 @@ func NewCmdSubmitUpgradeProposal() *cobra.Command { cmd.Flags().String(cli.FlagDescription, "", "description of proposal") cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") cmd.Flags().Int64(FlagUpgradeHeight, 0, "The height at which the upgrade must happen") - cmd.Flags().String(FlagUpgradeInfo, "", "Optional info for the planned upgrade such as commit hash, etc.") + cmd.Flags().String(FlagUpgradeInfo, "", "Info for the upgrade plan such as new version download urls, etc.") + cmd.Flags().Bool(FlagNoValidate, false, "Skip validation of the upgrade info") + cmd.Flags().String(FlagDaemonName, getDefaultDaemonName(), "The name of the executable being upgraded (for upgrade-info validation). Default is the DAEMON_NAME env var if set, or else this executable") return cmd } @@ -154,3 +180,15 @@ func parseArgsToContent(cmd *cobra.Command, name string) (gov.Content, error) { content := types.NewSoftwareUpgradeProposal(title, description, plan) return content, nil } + +// getDefaultDaemonName gets the default name to use for the daemon. +// If a DAEMON_NAME env var is set, that is used. +// Otherwise, the last part of the currently running executable is used. +func getDefaultDaemonName() string { + // DAEMON_NAME is specifically used here to correspond with the Comsovisor setup env vars. + name := os.Getenv("DAEMON_NAME") + if len(name) == 0 { + _, name = filepath.Split(os.Args[0]) + } + return name +} diff --git a/x/upgrade/plan/downloader.go b/x/upgrade/plan/downloader.go new file mode 100644 index 000000000000..0879b8badbfe --- /dev/null +++ b/x/upgrade/plan/downloader.go @@ -0,0 +1,141 @@ +package plan + +import ( + "errors" + "fmt" + neturl "net/url" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/go-getter" +) + +// DownloadUpgrade downloads the given url into the provided directory and ensures it's valid. +// The provided url must contain a checksum parameter that matches the file being downloaded. +// If this returns nil, the download was successful, and {dstRoot}/bin/{daemonName} is a regular executable file. +// This is an opinionated directory structure that corresponds with Cosmovisor requirements. +// If the url is not an archive, it is downloaded and saved to {dstRoot}/bin/{daemonName}. +// If the url is an archive, it is downloaded and unpacked to {dstRoot}. +// If the archive does not contain a /bin/{daemonName} file, then this will attempt to move /{daemonName} to /bin/{daemonName}. +// If the archive does not contain either /bin/{daemonName} or /{daemonName}, an error is returned. +// Note: Because a checksum is required, this function cannot be used to download non-archive directories. +// If dstRoot already exists, some or all of its contents might be updated. +func DownloadUpgrade(dstRoot, url, daemonName string) error { + if err := ValidateIsURLWithChecksum(url); err != nil { + return err + } + target := filepath.Join(dstRoot, "bin", daemonName) + // First try to download it as a single file. If there's no error, it's okay and we're done. + if err := getter.GetFile(target, url); err != nil { + // If it was a checksum error, no need to try as directory. + if _, ok := err.(*getter.ChecksumError); ok { + return err + } + // File download didn't work, try it as an archive. + if err = downloadUpgradeAsArchive(dstRoot, url, daemonName); err != nil { + // Out of options, send back the error. + return err + } + } + return EnsureBinary(target) +} + +// downloadUpgradeAsArchive tries to download the given url as an archive. +// The archive is unpacked and saved in dstDir. +// If the archive contains /{daemonName} and not /bin/{daemonName}, then /{daemonName} will be moved to /bin/{daemonName}. +// If this returns nil, the download was successful, and {dstDir}/bin/{daemonName} is a regular executable file. +func downloadUpgradeAsArchive(dstDir, url, daemonName string) error { + err := getter.Get(dstDir, url) + if err != nil { + return err + } + + // If bin/{daemonName} exists, we're done. + dstDirBinFile := filepath.Join(dstDir, "bin", daemonName) + err = EnsureBinary(dstDirBinFile) + if err == nil { + return nil + } + + // Otherwise, check for a root {daemonName} file and move it to the bin/ directory if found. + dstDirFile := filepath.Join(dstDir, daemonName) + err = EnsureBinary(dstDirFile) + if err == nil { + err = os.Rename(dstDirFile, dstDirBinFile) + if err != nil { + return fmt.Errorf("could not move %s to the bin directory: %w", daemonName, err) + } + return nil + } + + return fmt.Errorf("url \"%s\" result does not contain a bin/%s or %s file", url, daemonName, daemonName) +} + +// EnsureBinary checks that the given file exists as a regular file and is executable. +// An error is returned if: +// - The file does not exist. +// - The path exists, but is one of: Dir, Symlink, NamedPipe, Socket, Device, CharDevice, or Irregular. +// - The file exists, is not executable by all three of User, Group, and Other, and cannot be made executable. +func EnsureBinary(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if !info.Mode().IsRegular() { + _, f := filepath.Split(path) + return fmt.Errorf("%s is not a regular file", f) + } + // Make sure all executable bits are set. + oldMode := info.Mode().Perm() + newMode := oldMode | 0111 // Set the three execute bits to on (a+x). + if oldMode != newMode { + return os.Chmod(path, newMode) + } + return nil +} + +// DownloadURLWithChecksum gets the contents of the given url, ensuring the checksum is correct. +// The provided url must contain a checksum parameter that matches the file being downloaded. +// If there isn't an error, the content returned by the url will be returned as a string. +// Returns an error if: +// - The url is not a URL or does not contain a checksum parameter. +// - Downloading the URL fails. +// - The checksum does not match what is returned by the URL. +// - The URL does not return a regular file. +// - The downloaded file is empty or only whitespace. +func DownloadURLWithChecksum(url string) (string, error) { + if err := ValidateIsURLWithChecksum(url); err != nil { + return "", err + } + tempDir, err := os.MkdirTemp("", "reference") + if err != nil { + return "", fmt.Errorf("could not create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + tempFile := filepath.Join(tempDir, "content") + if err = getter.GetFile(tempFile, url); err != nil { + return "", fmt.Errorf("could not download url \"%s\": %w", url, err) + } + tempFileBz, rerr := os.ReadFile(tempFile) + if rerr != nil { + return "", fmt.Errorf("could not read downloaded temporary file: %w", rerr) + } + tempFileStr := strings.TrimSpace(string(tempFileBz)) + if len(tempFileStr) == 0 { + return "", fmt.Errorf("no content returned by \"%s\"", url) + } + return tempFileStr, nil +} + +// ValidateIsURLWithChecksum checks that the given string is a url and contains a checksum query parameter. +func ValidateIsURLWithChecksum(urlStr string) error { + url, err := neturl.Parse(urlStr) + if err != nil { + return err + } + if len(url.Query().Get("checksum")) == 0 { + return errors.New("missing checksum query parameter") + } + return nil +} diff --git a/x/upgrade/plan/downloader_test.go b/x/upgrade/plan/downloader_test.go new file mode 100644 index 000000000000..6e10b6de4c78 --- /dev/null +++ b/x/upgrade/plan/downloader_test.go @@ -0,0 +1,299 @@ +package plan + +import ( + "archive/zip" + "crypto/sha256" + "fmt" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type DownloaderTestSuite struct { + suite.Suite + + // Home is a temporary directory for use in these tests. + // It will have a src/ for things to download. + Home string +} + +func (s *DownloaderTestSuite) SetupTest() { + s.Home = s.T().TempDir() + s.Assert().NoError(os.MkdirAll(filepath.Join(s.Home, "src"), 0777), "creating src/ dir") + s.T().Logf("Home: [%s]", s.Home) +} + +func TestDownloaderTestSuite(t *testing.T) { + suite.Run(t, new(DownloaderTestSuite)) +} + +// TestFile represents a file that will be used for a test. +type TestFile struct { + // Name is the relative path and name of the file. + Name string + // Contents is the contents of the file. + Contents []byte +} + +func NewTestFile(name, contents string) *TestFile { + return &TestFile{ + Name: name, + Contents: []byte(contents), + } +} + +// SaveIn saves this TestFile in the given path. +// The full path to the file is returned. +func (f TestFile) SaveIn(path string) (string, error) { + name := filepath.Join(path, f.Name) + file, err := os.Create(name) + if err != nil { + return name, err + } + defer file.Close() + _, err = file.Write(f.Contents) + return name, err +} + +// TestZip represents a collection of TestFile objects to be zipped into an archive. +type TestZip []*TestFile + +func NewTestZip(testFiles ...*TestFile) TestZip { + tz := make([]*TestFile, len(testFiles)) + for i, tf := range testFiles { + tz[i] = tf + } + return tz +} + +// SaveAs saves this TestZip at the given path. +func (z TestZip) SaveAs(path string) error { + archive, err := os.Create(path) + if err != nil { + return err + } + defer archive.Close() + zipper := zip.NewWriter(archive) + for _, tf := range z { + zfw, zfwerr := zipper.Create(tf.Name) + if zfwerr != nil { + return zfwerr + } + _, err = zfw.Write(tf.Contents) + if err != nil { + return err + } + } + return zipper.Close() +} + +// saveTestZip saves a TestZip in this test's Home/src directory with the given name. +// The full path to the saved archive is returned. +func (s DownloaderTestSuite) saveSrcTestZip(name string, z TestZip) string { + fullName := filepath.Join(s.Home, "src", name) + s.Require().NoError(z.SaveAs(fullName), "saving test zip %s", name) + return fullName +} + +// saveSrcTestFile saves a TestFile in this test's Home/src directory. +// The full path to the saved file is returned. +func (s DownloaderTestSuite) saveSrcTestFile(f *TestFile) string { + path := filepath.Join(s.Home, "src") + fullName, err := f.SaveIn(path) + s.Require().NoError(err, "saving test file %s", f.Name) + return fullName +} + +// requireFileExistsAndIsExecutable requires that the given file exists and is executable. +func requireFileExistsAndIsExecutable(t *testing.T, path string) { + info, err := os.Stat(path) + require.NoError(t, err, "stat error") + perm := info.Mode().Perm() + // Checks if at least one executable bit is set (user, group, or other) + isExe := perm&0111 != 0 + require.True(t, isExe, "is executable: permissions = %s", perm) +} + +// requireFileEquals requires that the contents of the file at the given path +// is equal to the contents of the given TestFile. +func requireFileEquals(t *testing.T, path string, tf *TestFile) { + file, err := os.ReadFile(path) + require.NoError(t, err, "reading file") + require.Equal(t, string(tf.Contents), string(file), "file contents") +} + +// makeFileUrl converts the given path to a URL with the correct checksum query parameter. +func makeFileURL(t *testing.T, path string) string { + f, err := os.Open(path) + require.NoError(t, err, "opening file") + defer f.Close() + hasher := sha256.New() + _, err = io.Copy(hasher, f) + require.NoError(t, err, "copying file to hasher") + return fmt.Sprintf("file://%s?checksum=sha256:%x", path, hasher.Sum(nil)) +} + +func (s *DownloaderTestSuite) TestDownloadUpgrade() { + justAFile := NewTestFile("just-a-file", "#!/usr/bin\necho 'I am just a file'\n") + someFileName := "some-file" + someFileInBin := NewTestFile("bin"+someFileName, "#!/usr/bin\necho 'I am some file in bin'\n") + anotherFile := NewTestFile("another-file", "#!/usr/bin\necho 'I am just another file'\n") + justAFilePath := s.saveSrcTestFile(justAFile) + justAFileZip := s.saveSrcTestZip(justAFile.Name+".zip", NewTestZip(justAFile)) + someFileInBinZip := s.saveSrcTestZip(someFileInBin.Name+".zip", NewTestZip(someFileInBin)) + allFilesZip := s.saveSrcTestZip(anotherFile.Name+".zip", NewTestZip(justAFile, someFileInBin, anotherFile)) + getDstDir := func(testName string) string { + _, tName := filepath.Split(testName) + return s.Home + "/dst/" + tName + } + + s.T().Run("url does not exist", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + url := "file:///never/gonna/be/a/thing.zip?checksum=sha256:2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48" + err := DownloadUpgrade(dstRoot, url, "nothing") + require.Error(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + }) + + s.T().Run("url does not have checksum", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + url := "file://" + justAFilePath + err := DownloadUpgrade(dstRoot, url, justAFile.Name) + require.Error(t, err) + require.Contains(t, err.Error(), "missing checksum query parameter") + }) + + s.T().Run("url has incorrect checksum", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + badChecksum := "2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48" + url := "file://" + justAFilePath + "?checksum=sha256:" + badChecksum + err := DownloadUpgrade(dstRoot, url, justAFile.Name) + require.Error(t, err) + assert.Contains(t, err.Error(), "Checksums did not match") + assert.Contains(t, err.Error(), "Expected: "+badChecksum) + }) + + s.T().Run("url returns single file", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + url := makeFileURL(t, justAFilePath) + err := DownloadUpgrade(dstRoot, url, justAFile.Name) + require.NoError(t, err) + expectedFile := filepath.Join(dstRoot, "bin", justAFile.Name) + requireFileExistsAndIsExecutable(t, expectedFile) + requireFileEquals(t, expectedFile, justAFile) + }) + + s.T().Run("url returns archive with file in bin", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + url := makeFileURL(t, someFileInBinZip) + err := DownloadUpgrade(dstRoot, url, someFileName) + require.NoError(t, err) + expectedFile := filepath.Join(dstRoot, "bin", someFileName) + requireFileExistsAndIsExecutable(t, expectedFile) + requireFileEquals(t, expectedFile, someFileInBin) + }) + + s.T().Run("url returns archive with just expected file", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + url := makeFileURL(t, justAFileZip) + err := DownloadUpgrade(dstRoot, url, justAFile.Name) + require.NoError(t, err) + expectedFile := filepath.Join(dstRoot, "bin", justAFile.Name) + requireFileExistsAndIsExecutable(t, expectedFile) + requireFileEquals(t, expectedFile, justAFile) + }) + + s.T().Run("url returns archive without expected file", func(t *testing.T) { + dstRoot := getDstDir(t.Name()) + url := makeFileURL(t, allFilesZip) + err := DownloadUpgrade(dstRoot, url, "not-expected") + require.Error(t, err) + require.Contains(t, err.Error(), "result does not contain a bin/not-expected or not-expected file") + }) +} + +func (s *DownloaderTestSuite) TestEnsureBinary() { + nonExeName := s.saveSrcTestFile(NewTestFile("non-exe.txt", "Not executable")) + s.Require().NoError(os.Chmod(nonExeName, 0600), "chmod error nonExeName") + isExeName := s.saveSrcTestFile(NewTestFile("is-exe.sh", "#!/bin/bash\necho 'executing'\n")) + s.Require().NoError(os.Chmod(isExeName, 0777), "chmod error isExeName") + + s.T().Run("file does not exist", func(t *testing.T) { + name := filepath.Join(s.Home, "does-not-exist.txt") + actual := EnsureBinary(name) + require.Error(t, actual) + }) + + s.T().Run("file is a directory", func(t *testing.T) { + name := filepath.Join(s.Home, "src") + actual := EnsureBinary(name) + require.EqualError(t, actual, fmt.Sprintf("%s is not a regular file", "src")) + }) + + s.T().Run("file exists and becomes executable", func(t *testing.T) { + name := nonExeName + actual := EnsureBinary(name) + require.NoError(t, actual, "EnsureBinary error") + requireFileExistsAndIsExecutable(t, name) + }) + + s.T().Run("file is already executable", func(t *testing.T) { + name := isExeName + actual := EnsureBinary(name) + require.NoError(t, actual, "EnsureBinary error") + requireFileExistsAndIsExecutable(t, name) + }) +} + +func (s *DownloaderTestSuite) TestDownloadURLWithChecksum() { + planContents := `{"binaries":{"xxx/yyy":"url"}}` + planFile := NewTestFile("plan-info.json", planContents) + planPath := s.saveSrcTestFile(planFile) + planChecksum := fmt.Sprintf("%x", sha256.Sum256(planFile.Contents)) + emptyFile := NewTestFile("empty-plan-info.json", "") + emptyPlanPath := s.saveSrcTestFile(emptyFile) + emptyChecksum := fmt.Sprintf("%x", sha256.Sum256(emptyFile.Contents)) + + s.T().Run("url does not exist", func(t *testing.T) { + url := "file:///never-gonna-be-a-thing?checksum=sha256:2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48" + _, err := DownloadURLWithChecksum(url) + require.Error(t, err) + assert.Contains(t, err.Error(), "could not download url") + }) + + s.T().Run("without checksum", func(t *testing.T) { + url := "file://" + planPath + _, err := DownloadURLWithChecksum(url) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing checksum query parameter") + }) + + s.T().Run("with correct checksum", func(t *testing.T) { + url := "file://" + planPath + "?checksum=sha256:" + planChecksum + actual, err := DownloadURLWithChecksum(url) + require.NoError(t, err) + require.Equal(t, planContents, actual) + }) + + s.T().Run("with incorrect checksum", func(t *testing.T) { + badChecksum := "2c22e34510bd1d4ad2343cdc54f7165bccf30caef73f39af7dd1db2795a3da48" + url := "file://" + planPath + "?checksum=sha256:" + badChecksum + _, err := DownloadURLWithChecksum(url) + require.Error(t, err) + assert.Contains(t, err.Error(), "Checksums did not match") + assert.Contains(t, err.Error(), "Expected: "+badChecksum) + assert.Contains(t, err.Error(), "Got: "+planChecksum) + }) + + s.T().Run("plan is empty", func(t *testing.T) { + url := "file://" + emptyPlanPath + "?checksum=sha256:" + emptyChecksum + _, err := DownloadURLWithChecksum(url) + require.Error(t, err) + assert.Contains(t, err.Error(), "no content returned") + }) +} diff --git a/x/upgrade/plan/info.go b/x/upgrade/plan/info.go new file mode 100644 index 000000000000..5f146a9c9a48 --- /dev/null +++ b/x/upgrade/plan/info.go @@ -0,0 +1,106 @@ +package plan + +import ( + "encoding/json" + "errors" + "fmt" + neturl "net/url" + "os" + "path/filepath" + "regexp" + "strings" +) + +// Info is the special structure that the Plan.Info string can be (as json). +type Info struct { + Binaries BinaryDownloadURLMap `json:"binaries"` +} + +// BinaryDownloadURLMap is a map of os/architecture stings to a URL where the binary can be downloaded. +type BinaryDownloadURLMap map[string]string + +// ParseInfo parses an info string into a map of os/arch strings to URL string. +// If the infoStr is a url, an GET request will be made to it, and its response will be parsed instead. +func ParseInfo(infoStr string) (*Info, error) { + infoStr = strings.TrimSpace(infoStr) + + if len(infoStr) == 0 { + return nil, errors.New("plan info must not be blank") + } + + // If it's a url, download it and treat the result as the real info. + if _, err := neturl.Parse(infoStr); err == nil { + infoStr, err = DownloadURLWithChecksum(infoStr) + if err != nil { + return nil, err + } + } + + // Now, try to parse it into the expected structure. + var planInfo Info + if err := json.Unmarshal([]byte(infoStr), &planInfo); err != nil { + return nil, fmt.Errorf("could not parse plan info: %v", err) + } + + return &planInfo, nil +} + +// ValidateFull does all possible validation of this Info. +// The provided daemonName is the name of the executable file expected in all downloaded directories. +// It checks that: +// * Binaries.ValidateBasic() doesn't return an error +// * Binaries.CheckURLs(daemonName) doesn't return an error. +// Warning: This is an expensive process. See BinaryDownloadURLMap.CheckURLs for more info. +func (m Info) ValidateFull(daemonName string) error { + if err := m.Binaries.ValidateBasic(); err != nil { + return err + } + if err := m.Binaries.CheckURLs(daemonName); err != nil { + return err + } + return nil +} + +// ValidateBasic does stateless validation of this BinaryDownloadURLMap. +// It validates that: +// * This has at least one entry. +// * All entry keys have the format "os/arch" or are "any". +// * All entry values are valid URLs. +// * All URLs contain a checksum query parameter. +func (m BinaryDownloadURLMap) ValidateBasic() error { + // Make sure there's at least one. + if len(m) == 0 { + return errors.New("no \"binaries\" entries found") + } + + osArchRx := regexp.MustCompile(`[a-zA-Z0-9]+/[a-zA-Z0-9]+`) + for key, val := range m { + if key != "any" && !osArchRx.MatchString(key) { + return fmt.Errorf("invalid os/arch format in key \"%s\"", key) + } + if err := ValidateIsURLWithChecksum(val); err != nil { + return fmt.Errorf("invalid url \"%s\" in binaries[%s]: %v", val, key, err) + } + } + + return nil +} + +// CheckURLs checks that all entries have valid URLs that return expected data. +// The provided daemonName is the name of the executable file expected in all downloaded directories. +// Warning: This is an expensive process. +// It will make an HTTP GET request to each URL and download the response. +func (m BinaryDownloadURLMap) CheckURLs(daemonName string) error { + tempDir, err := os.MkdirTemp("", "os-arch-downloads") + if err != nil { + return fmt.Errorf("could not create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + for osArch, url := range m { + dstRoot := filepath.Join(tempDir, strings.ReplaceAll(osArch, "/", "-")) + if err = DownloadUpgrade(dstRoot, url, daemonName); err != nil { + return fmt.Errorf("error downloading binary for os/arch %s: %v", osArch, err) + } + } + return nil +} diff --git a/x/upgrade/plan/info_test.go b/x/upgrade/plan/info_test.go new file mode 100644 index 000000000000..65736a5e67f6 --- /dev/null +++ b/x/upgrade/plan/info_test.go @@ -0,0 +1,335 @@ +package plan + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type InfoTestSuite struct { + suite.Suite + + // Home is a temporary directory for use in these tests. + Home string +} + +func (s *InfoTestSuite) SetupTest() { + s.Home = s.T().TempDir() + s.T().Logf("Home: [%s]", s.Home) +} + +func TestInfoTestSuite(t *testing.T) { + suite.Run(t, new(InfoTestSuite)) +} + +// saveSrcTestFile saves a TestFile in this test's Home/src directory. +// The full path to the saved file is returned. +func (s InfoTestSuite) saveTestFile(f *TestFile) string { + fullName, err := f.SaveIn(s.Home) + s.Require().NoError(err, "saving test file %s", f.Name) + return fullName +} + +func (s InfoTestSuite) TestParseInfo() { + goodJSON := `{"binaries":{"os1/arch1":"url1","os2/arch2":"url2"}}` + binariesWrongJSON := `{"binaries":["foo","bar"]}` + binariesWrongValueJSON := `{"binaries":{"os1/arch1":1,"os2/arch2":2}}` + goodJSONPath := s.saveTestFile(NewTestFile("good.json", goodJSON)) + binariesWrongJSONPath := s.saveTestFile(NewTestFile("binaries-wrong.json", binariesWrongJSON)) + binariesWrongValueJSONPath := s.saveTestFile(NewTestFile("binaries-wrong-value.json", binariesWrongValueJSON)) + goodJSONAsInfo := &Info{ + Binaries: BinaryDownloadURLMap{ + "os1/arch1": "url1", + "os2/arch2": "url2", + }, + } + makeInfoStrFuncString := func(val string) func(t *testing.T) string { + return func(t *testing.T) string { + return val + } + } + makeInfoStrFuncURL := func(file string) func(t *testing.T) string { + return func(t *testing.T) string { + return makeFileURL(t, file) + } + } + + tests := []struct { + name string + infoStrMaker func(t *testing.T) string + expectedInfo *Info + expectedInError []string + }{ + { + name: "json good", + infoStrMaker: makeInfoStrFuncString(goodJSON), + expectedInfo: goodJSONAsInfo, + expectedInError: nil, + }, + { + name: "blank string", + infoStrMaker: makeInfoStrFuncString(" "), + expectedInfo: nil, + expectedInError: []string{"plan info must not be blank"}, + }, + { + name: "json binaries is wrong data type", + infoStrMaker: makeInfoStrFuncString(binariesWrongJSON), + expectedInfo: nil, + expectedInError: []string{"could not parse plan info", "cannot unmarshal array into Go struct field Info.binaries"}, + }, + { + name: "json wrong data type in binaries value", + infoStrMaker: makeInfoStrFuncString(binariesWrongValueJSON), + expectedInfo: nil, + expectedInError: []string{"could not parse plan info", "cannot unmarshal number into Go struct field Info.binaries"}, + }, + { + name: "url does not exist", + infoStrMaker: makeInfoStrFuncString("file:///this/file/does/not/exist?checksum=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), + expectedInfo: nil, + expectedInError: []string{"could not download url", "file:///this/file/does/not/exist"}, + }, + { + name: "url good", + infoStrMaker: makeInfoStrFuncURL(goodJSONPath), + expectedInfo: goodJSONAsInfo, + expectedInError: nil, + }, + { + name: "url binaries is wrong data type", + infoStrMaker: makeInfoStrFuncURL(binariesWrongJSONPath), + expectedInfo: nil, + expectedInError: []string{"could not parse plan info", "cannot unmarshal array into Go struct field Info.binaries"}, + }, + { + name: "url wrong data type in binaries value", + infoStrMaker: makeInfoStrFuncURL(binariesWrongValueJSONPath), + expectedInfo: nil, + expectedInError: []string{"could not parse plan info", "cannot unmarshal number into Go struct field Info.binaries"}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + infoStr := tc.infoStrMaker(t) + actualInfo, actualErr := ParseInfo(infoStr) + if len(tc.expectedInError) > 0 { + require.Error(t, actualErr) + for _, expectedErr := range tc.expectedInError { + assert.Contains(t, actualErr.Error(), expectedErr) + } + } else { + require.NoError(t, actualErr) + } + assert.Equal(t, tc.expectedInfo, actualInfo) + }) + } +} + +func (s InfoTestSuite) TestInfoValidateFull() { + darwinAMD64File := NewTestFile("darwin_amd64", "#!/usr/bin\necho 'darwin/amd64'\n") + linux386File := NewTestFile("linux_386", "#!/usr/bin\necho 'darwin/amd64'\n") + darwinAMD64Path := s.saveTestFile(darwinAMD64File) + linux386Path := s.saveTestFile(linux386File) + darwinAMD64URL := makeFileURL(s.T(), darwinAMD64Path) + linux386URL := makeFileURL(s.T(), linux386Path) + + tests := []struct { + name string + planInfo *Info + errs []string + }{ + // Positive test case + { + name: "two good entries", + planInfo: &Info{ + Binaries: BinaryDownloadURLMap{ + "darwin/amd64": darwinAMD64URL, + "linux/386": linux386URL, + }, + }, + errs: nil, + }, + // a failure from BinaryDownloadURLMap.ValidateBasic + { + name: "empty binaries", + planInfo: &Info{Binaries: BinaryDownloadURLMap{}}, + errs: []string{"no \"binaries\" entries found"}, + }, + // a failure from BinaryDownloadURLMap.CheckURLS + { + name: "url does not exist", + planInfo: &Info{ + Binaries: BinaryDownloadURLMap{ + "darwin/arm64": "file:///no/such/file/exists/hopefully.zip?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", + }, + }, + errs: []string{"error downloading binary", "darwin/arm64", "no such file or directory"}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + actualErr := tc.planInfo.ValidateFull("daemon") + if len(tc.errs) > 0 { + require.Error(t, actualErr) + for _, expectedErr := range tc.errs { + assert.Contains(t, actualErr.Error(), expectedErr) + } + } else { + require.NoError(t, actualErr) + } + }) + } +} + +func (s InfoTestSuite) TestBinaryDownloadURLMapValidateBasic() { + addDummyChecksum := func(url string) string { + return url + "?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259" + } + tests := []struct { + name string + urlMap BinaryDownloadURLMap + errs []string + }{ + { + name: "empty map", + urlMap: BinaryDownloadURLMap{}, + errs: []string{"no \"binaries\" entries found"}, + }, + { + name: "key with empty string", + urlMap: BinaryDownloadURLMap{ + "": addDummyChecksum("https://v1.cosmos.network/sdk"), + }, + errs: []string{"invalid os/arch", `""`}, + }, + { + name: "invalid key format", + urlMap: BinaryDownloadURLMap{ + "badkey": addDummyChecksum("https://v1.cosmos.network/sdk"), + }, + errs: []string{"invalid os/arch", "badkey"}, + }, + { + name: "any key is valid", + urlMap: BinaryDownloadURLMap{ + "any": addDummyChecksum("https://v1.cosmos.network/sdk"), + }, + errs: nil, + }, + { + name: "os arch key is valid", + urlMap: BinaryDownloadURLMap{ + "darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"), + }, + errs: nil, + }, + { + name: "not a url", + urlMap: BinaryDownloadURLMap{ + "isa/url": addDummyChecksum("https://v1.cosmos.network/sdk"), + "nota/url": addDummyChecksum("https://v1.cosmos.network:not-a-port/sdk"), + }, + errs: []string{"invalid url", "nota/url", "invalid port"}, + }, + { + name: "url without checksum", + urlMap: BinaryDownloadURLMap{ + "darwin/amd64": "https://v1.cosmos.network/sdk", + }, + errs: []string{"invalid url", "darwin/amd64", "missing checksum query parameter"}, + }, + { + name: "multiple valid entries but one bad url", + urlMap: BinaryDownloadURLMap{ + "any": addDummyChecksum("https://v1.cosmos.network/sdk"), + "darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"), + "darwin/arm64": addDummyChecksum("https://v1.cosmos.network/sdk"), + "windows/bad": addDummyChecksum("https://v1.cosmos.network:not-a-port/sdk"), + "linux/386": addDummyChecksum("https://v1.cosmos.network/sdk"), + }, + errs: []string{"invalid url", "windows/bad", "invalid port"}, + }, + { + name: "multiple valid entries but one bad key", + urlMap: BinaryDownloadURLMap{ + "any": addDummyChecksum("https://v1.cosmos.network/sdk"), + "darwin/amd64": addDummyChecksum("https://v1.cosmos.network/sdk"), + "badkey": addDummyChecksum("https://v1.cosmos.network/sdk"), + "darwin/arm64": addDummyChecksum("https://v1.cosmos.network/sdk"), + "linux/386": addDummyChecksum("https://v1.cosmos.network/sdk"), + }, + errs: []string{"invalid os/arch", "badkey"}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + actualErr := tc.urlMap.ValidateBasic() + if len(tc.errs) > 0 { + require.Error(t, actualErr) + for _, expectedErr := range tc.errs { + assert.Contains(t, actualErr.Error(), expectedErr) + } + } else { + require.NoError(t, actualErr) + } + }) + } +} + +func (s InfoTestSuite) TestBinaryDownloadURLMapCheckURLs() { + darwinAMD64File := NewTestFile("darwin_amd64", "#!/usr/bin\necho 'darwin/amd64'\n") + linux386File := NewTestFile("linux_386", "#!/usr/bin\necho 'darwin/amd64'\n") + darwinAMD64Path := s.saveTestFile(darwinAMD64File) + linux386Path := s.saveTestFile(linux386File) + darwinAMD64URL := makeFileURL(s.T(), darwinAMD64Path) + linux386URL := makeFileURL(s.T(), linux386Path) + + tests := []struct { + name string + urlMap BinaryDownloadURLMap + errs []string + }{ + { + name: "two good entries", + urlMap: BinaryDownloadURLMap{ + "darwin/amd64": darwinAMD64URL, + "linux/386": linux386URL, + }, + errs: nil, + }, + { + name: "url does not exist", + urlMap: BinaryDownloadURLMap{ + "darwin/arm64": "file:///no/such/file/exists/hopefully.zip?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", + }, + errs: []string{"error downloading binary", "darwin/arm64", "no such file or directory"}, + }, + { + name: "bad checksum", + urlMap: BinaryDownloadURLMap{ + "darwin/amd64": "file://" + darwinAMD64Path + "?checksum=sha256:b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", + }, + errs: []string{"error downloading binary", "darwin/amd64", "Checksums did not match", "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259"}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + actualErr := tc.urlMap.CheckURLs("daemon") + if len(tc.errs) > 0 { + require.Error(t, actualErr) + for _, expectedErr := range tc.errs { + assert.Contains(t, actualErr.Error(), expectedErr) + } + } else { + require.NoError(t, actualErr) + } + }) + } +}