diff --git a/.changelog/unreleased/features/463-integrate-jester.md b/.changelog/unreleased/features/463-integrate-jester.md new file mode 100644 index 00000000..c76bfcd7 --- /dev/null +++ b/.changelog/unreleased/features/463-integrate-jester.md @@ -0,0 +1 @@ +- Integrate our custom Jester sidecar, that enables the automatic relaying of $USDN transfers to Noble. ([#463](https://github.com/noble-assets/noble/pull/463)) diff --git a/.gitignore b/.gitignore index c872e916..0c361846 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ play_sh/ /heighliner* bin/ build/ +.duke/ go.work go.work.sum diff --git a/app.go b/app.go index 6a559b59..cc627223 100644 --- a/app.go +++ b/app.go @@ -23,6 +23,8 @@ import ( "os" "path/filepath" + "github.com/spf13/cast" + "cosmossdk.io/core/appconfig" "cosmossdk.io/depinject" "cosmossdk.io/log" @@ -36,6 +38,7 @@ import ( servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/noble-assets/noble/v9/jester" "github.com/noble-assets/noble/v9/upgrade" _ "cosmossdk.io/x/evidence" @@ -272,6 +275,16 @@ func NewApp( } app.SetAnteHandler(anteHandler) + jesterClient := jester.NewClient(cast.ToString(appOpts.Get(jester.FlagGRPCAddress))) + proposalHandler := NewProposalHandler( + app.BaseApp, app.Mempool(), app.PreBlocker, + jesterClient, app.DollarKeeper, app.WormholeKeeper, + ) + + app.SetPrepareProposal(proposalHandler.PrepareProposal()) + app.SetProcessProposal(proposalHandler.ProcessProposal()) + app.SetPreBlocker(proposalHandler.PreBlocker()) + if err := app.RegisterUpgradeHandler(); err != nil { return nil, err } diff --git a/cmd/commands.go b/cmd/commands.go index 6804c36e..b7137560 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -38,6 +38,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/crisis" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" "github.com/noble-assets/noble/v9" + "github.com/noble-assets/noble/v9/jester" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -55,6 +56,7 @@ func initRootCmd(rootCmd *cobra.Command, txConfig client.TxConfig, basicManager ) server.AddCommands(rootCmd, noble.DefaultNodeHome, newApp, appExport, func(startCmd *cobra.Command) { + jester.AddFlags(startCmd) crisis.AddModuleInitFlags(startCmd) }) diff --git a/cmd/root.go b/cmd/root.go index adac1279..a5802b9d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,6 +28,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/tx/signing" "github.com/cosmos/cosmos-sdk/x/auth/tx" txmodule "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + "github.com/noble-assets/noble/v9/jester" "github.com/spf13/cobra" ) @@ -78,7 +79,9 @@ func NewRootCmd() *cobra.Command { cmtCfg := cmtcfg.DefaultConfig() cmtCfg.Consensus.TimeoutCommit = 500 * time.Millisecond - return server.InterceptConfigsPreRunHandler(cmd, serverconfig.DefaultConfigTemplate, srvCfg, cmtCfg) + customAppTemplate, appConfig := jester.AppendJesterConfig(srvCfg) + + return server.InterceptConfigsPreRunHandler(cmd, customAppTemplate, appConfig, cmtCfg) }, } diff --git a/go.mod b/go.mod index 1c66e240..5cc7eb94 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/noble-assets/noble/v9 go 1.22.11 require ( + connectrpc.com/connect v1.18.1 cosmossdk.io/client/v2 v2.0.0-beta.8 cosmossdk.io/core v0.11.1 cosmossdk.io/depinject v1.1.0 @@ -29,10 +30,13 @@ require ( github.com/noble-assets/forwarding/v2 v2.0.0 github.com/noble-assets/globalfee v1.0.0 github.com/noble-assets/halo/v2 v2.0.1 - github.com/noble-assets/wormhole v1.0.0-alpha.0 + github.com/noble-assets/wormhole v1.0.0-alpha.2 github.com/ondoprotocol/usdy-noble/v2 v2.0.0 + github.com/spf13/cast v1.7.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 + github.com/wormhole-foundation/wormhole/sdk v0.0.0-20241218143724-3797ed082150 + jester.noble.xyz/api v0.1.0 mvdan.cc/gofumpt v0.7.0 swap.noble.xyz v1.0.0-alpha.3 ) @@ -289,7 +293,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect @@ -309,7 +312,6 @@ require ( github.com/ultraware/funlen v0.1.0 // indirect github.com/ultraware/whitespace v0.1.1 // indirect github.com/uudashr/gocognit v1.1.3 // indirect - github.com/wormhole-foundation/wormhole/sdk v0.0.0-20241218143724-3797ed082150 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect diff --git a/go.sum b/go.sum index b2417e2b..078e4709 100644 --- a/go.sum +++ b/go.sum @@ -194,6 +194,8 @@ cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xX cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= cosmossdk.io/client/v2 v2.0.0-beta.8 h1:RXMJdA4V9H1H3/3BfMD6dAW3lF8W9DpNPPYnKD+ArxY= @@ -1092,8 +1094,8 @@ github.com/noble-assets/globalfee v1.0.0 h1:NUNDXd5tdzB5A/O8Em/1g+GU92E4lojH3+3l github.com/noble-assets/globalfee v1.0.0/go.mod h1:DmNoTJ2LqGP4KpJuz+IEKp/5uf/3hRu3GSNBGhNUZkA= github.com/noble-assets/halo/v2 v2.0.1 h1:nHAhTnq5dPJGelcLnKzMviXtk9x0DfMnRPv+CPoEvyA= github.com/noble-assets/halo/v2 v2.0.1/go.mod h1:DY4GCfZ/7S3IEjoJBCCh7HRTxirPBOLMVwkT0N6n3bA= -github.com/noble-assets/wormhole v1.0.0-alpha.0 h1:SEZvL0yT/tOFbBxzMVebl/Zaqc9VbuLnd0ER8wNyX40= -github.com/noble-assets/wormhole v1.0.0-alpha.0/go.mod h1:OUivFiRSS9o7k4q/5rBBZEwh0hR8kzoDVD7LulOkHx0= +github.com/noble-assets/wormhole v1.0.0-alpha.2 h1:LWNGDez+j4XP68Ak/SdCEtH7OUVYIAhAc/iovbuGLM0= +github.com/noble-assets/wormhole v1.0.0-alpha.2/go.mod h1:s7T9FC+bxHwlLED8pmXumdqwhr56CGJqTdhhcIj4A/Q= github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -2119,6 +2121,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= +jester.noble.xyz/api v0.1.0 h1:LgMVvOH/xgw0PDzCP4Sqsli+Ead+3TmOBKtBpVM1XV0= +jester.noble.xyz/api v0.1.0/go.mod h1:4TGQUpQF7xAW7xRLUydVkkrEgQNKDh545IGClJvgx4E= mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= diff --git a/jester/client.go b/jester/client.go new file mode 100644 index 00000000..b48a2993 --- /dev/null +++ b/jester/client.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 NASD Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jester + +import ( + "net/http" + "strings" + + jester "jester.noble.xyz/api" +) + +func NewClient(address string) jester.QueryServiceClient { + if !strings.Contains(address, "://") { + address = "http://" + address + } + + return jester.NewQueryServiceClient( + http.DefaultClient, + address, + ) +} diff --git a/jester/config.go b/jester/config.go new file mode 100644 index 00000000..979706d7 --- /dev/null +++ b/jester/config.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 NASD Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jester + +import ( + serverconfig "github.com/cosmos/cosmos-sdk/server/config" + "github.com/spf13/cobra" +) + +const ( + defaultJesterAddress = "localhost:9091" +) + +// AppendJesterConfig appends the Jester configuration to app.toml +func AppendJesterConfig(srvCfg *serverconfig.Config) (customAppTemplate string, NobleAppConfig interface{}) { + type JesterConfig struct { + GRPCAddress string `mapstructure:"grpc-address"` + } + + type CustomAppConfig struct { + serverconfig.Config + + JesterConfig JesterConfig `mapstructure:"jester"` + } + + defaultJesterConfig := JesterConfig{ + GRPCAddress: defaultJesterAddress, + } + + NobleAppConfig = CustomAppConfig{Config: *srvCfg, JesterConfig: defaultJesterConfig} + + customAppTemplate = serverconfig.DefaultConfigTemplate + ` +############################################################################### +### Jester (sidecar) ### +############################################################################### + +[jester] + +# Jester's gRPC server address. +# This should not conflict with the CometBFT gRPC server. +grpc-address = "{{ .JesterConfig.GRPCAddress }}" +` + return customAppTemplate, NobleAppConfig +} + +// Flags + +const ( + FlagGRPCAddress = "jester.grpc-address" +) + +func AddFlags(cmd *cobra.Command) { + cmd.Flags().String(FlagGRPCAddress, defaultJesterAddress, "Jester's gRPC server address") +} diff --git a/local.sh b/local.sh old mode 100644 new mode 100755 index 272f1b9d..267506fb --- a/local.sh +++ b/local.sh @@ -26,6 +26,7 @@ if ! [ -f .duke/data/priv_validator_state.json ]; then touch $TEMP && jq '.app_state.bank.denom_metadata += [{ "description": "Hashnote US Yield Coin", "denom_units": [{ "denom": "uusyc", "exponent": 0, "aliases": ["microusyc"] }, { "denom": "usyc", "exponent": 6 }], "base": "uusyc", "display": "usyc", "name": "Hashnote US Yield Coin", "symbol": "USYC" }]' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json touch $TEMP && jq '.app_state.bank.denom_metadata += [{ "description": "Monerium EUR emoney", "denom_units": [{ "denom": "ueure", "exponent": 0, "aliases": ["microeure"] }, { "denom": "eure", "exponent": 6 }], "base": "ueure", "display": "eure", "name": "Monerium EUR emoney", "symbol": "EURe" }]' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json touch $TEMP && jq '.app_state."fiat-tokenfactory".mintingDenom = { "denom": "uusdc" }' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json + touch $TEMP && jq '.app_state."fiat-tokenfactory".paused.paused = false' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json touch $TEMP && jq '.app_state.staking.params.bond_denom = "ustake"' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json touch $TEMP && jq '.app_state.wormhole.config.chain_id = 4009' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json touch $TEMP && jq '.app_state.wormhole.config.gov_chain = 1' .duke/config/genesis.json > $TEMP && mv $TEMP .duke/config/genesis.json diff --git a/local_3Val.sh b/local_3Val.sh new file mode 100755 index 00000000..098623a7 --- /dev/null +++ b/local_3Val.sh @@ -0,0 +1,114 @@ +alias nobled=./build/nobled + +for arg in "$@" +do + case $arg in + -r|--reset) + rm -rf .duke + shift + ;; + esac +done + +HOME1=.duke/val1 +HOME2=.duke/val2 +HOME3=.duke/val3 + +P2P1=0.0.0.0:26656 +P2P2=0.0.0.0:36656 +P2P3=0.0.0.0:46656 + + # if private validator file does not exist, create a new network +if ! [ -f .duke/data/priv_validator_state.json ]; then + nobled init val1 --chain-id "duke-1" --home $HOME1 &> /dev/null + nobled init val2 --chain-id "duke-1" --home $HOME2 &> /dev/null + nobled init val3 --chain-id "duke-1" --home $HOME3 &> /dev/null + + # Create keys + nobled keys add val --keyring-backend test --home $HOME1 &> /dev/null + nobled keys add val --keyring-backend test --home $HOME2 &> /dev/null + nobled keys add val --keyring-backend test --home $HOME3 &> /dev/null + + # Add genesis accounts from each validator + nobled genesis add-genesis-account val 1000000ustake --home $HOME1 --keyring-backend test + nobled genesis add-genesis-account val 1000000ustake --home $HOME2 --keyring-backend test + nobled genesis add-genesis-account val 1000000ustake --home $HOME3 --keyring-backend test + # Add genesis accounts to validator 1 who will be collecting genesis + nobled genesis add-genesis-account "$(nobled keys show val -a --keyring-backend test --home $HOME2)" 1000000ustake --home $HOME1 + nobled genesis add-genesis-account "$(nobled keys show val -a --keyring-backend test --home $HOME3)" 1000000ustake --home $HOME1 + + # Create genesis transaction's + nobled genesis gentx val 1000000ustake --chain-id "duke-1" --keyring-backend test --home $HOME1 + nobled genesis gentx val 1000000ustake --chain-id "duke-1" --output-document $HOME1/config/gentx/val2.json --keyring-backend test --home $HOME2 + nobled genesis gentx val 1000000ustake --chain-id "duke-1" --output-document $HOME1/config/gentx/val3.json --keyring-backend test --home $HOME3 + + # Collect the gentx and finalize genesis + nobled genesis collect-gentxs --home $HOME1 &> /dev/null + + AUTHORITY=$(nobled keys add authority --home $HOME1 --keyring-backend test --output json | jq .address) + nobled genesis add-genesis-account authority 4000000ustake --home $HOME1 --keyring-backend test + + TEMP=$HOME1/genesis.json + touch $TEMP && jq '.app_state.authority.owner = '$AUTHORITY'' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.bank.denom_metadata += [{ "description": "Circle USD Coin", "denom_units": [{ "denom": "uusdc", "exponent": 0, "aliases": ["microusdc"] }, { "denom": "usdc", "exponent": 6 }], "base": "uusdc", "display": "usdc", "name": "Circle USD Coin", "symbol": "USDC" }]' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.bank.denom_metadata += [{ "description": "Ondo US Dollar Yield", "denom_units": [{ "denom": "ausdy", "exponent": 0, "aliases": ["attousdy"] }, { "denom": "usdy", "exponent": 18 }], "base": "ausdy", "display": "usdy", "name": "Ondo US Dollar Yield", "symbol": "USDY" }]' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.bank.denom_metadata += [{ "description": "Hashnote US Yield Coin", "denom_units": [{ "denom": "uusyc", "exponent": 0, "aliases": ["microusyc"] }, { "denom": "usyc", "exponent": 6 }], "base": "uusyc", "display": "usyc", "name": "Hashnote US Yield Coin", "symbol": "USYC" }]' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.bank.denom_metadata += [{ "description": "Monerium EUR emoney", "denom_units": [{ "denom": "ueure", "exponent": 0, "aliases": ["microeure"] }, { "denom": "eure", "exponent": 6 }], "base": "ueure", "display": "eure", "name": "Monerium EUR emoney", "symbol": "EURe" }]' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state."fiat-tokenfactory".paused.paused = false' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state."fiat-tokenfactory".mintingDenom = { "denom": "uusdc" }' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.staking.params.bond_denom = "ustake"' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.wormhole.config.chain_id = 4009' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.wormhole.config.gov_chain = 1' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.wormhole.config.gov_address = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ="' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + touch $TEMP && jq '.app_state.wormhole.guardian_sets = {"0":{"addresses":["vvpCnVfNGLf4pNkaLamrSvBdD74="],"expiration_time":0}}' $HOME1/config/genesis.json > $TEMP && mv $TEMP $HOME1/config/genesis.json + + # Copy genesis to val 2 and 3 + cp "$HOME1/config/genesis.json" "$HOME2/config/genesis.json" + cp "$HOME1/config/genesis.json" "$HOME3/config/genesis.json" + + # Configure config.toml setting not available in a flag + sed -i '' 's|addr_book_strict = true|addr_book_strict = false|' $HOME1/config/config.toml + sed -i '' 's|addr_book_strict = true|addr_book_strict = false|' $HOME2/config/config.toml + sed -i '' 's|addr_book_strict = true|addr_book_strict = false|' $HOME3/config/config.toml + + sed -i '' 's|allow_duplicate_ip = false|allow_duplicate_ip = true|' $HOME1/config/config.toml + sed -i '' 's|allow_duplicate_ip = false|allow_duplicate_ip = true|' $HOME2/config/config.toml + sed -i '' 's|allow_duplicate_ip = false|allow_duplicate_ip = true|' $HOME3/config/config.toml +fi + +# Get persistent peers +NODE_ID1=$(nobled tendermint show-node-id --home "$HOME1") +PP1="$NODE_ID1@$P2P1" + +NODE_ID2=$(nobled tendermint show-node-id --home "$HOME2") +PP2="$NODE_ID2@$P2P2" + +NODE_ID3=$(nobled tendermint show-node-id --home "$HOME3") +PP3="$NODE_ID3@$P2P3" + +# Start tmux session +SESSION="3v-network" +tmux new-session -d -s "$SESSION" + +tmux split-window -h -t "$SESSION" +tmux split-window -h -t "$SESSION" + +# Send start command +# Note: C-m is equivalent to pressing Enter +tmux send-keys -t "$SESSION:0.0" "./build/nobled start --api.enable false --home $HOME1 > $HOME1/logs.log 2>&1 &" C-m +tmux send-keys -t "$SESSION:0.1" "./build/nobled start --api.enable false --rpc.laddr tcp://127.0.0.1:36657 --rpc.pprof_laddr localhost:6061 --grpc.address localhost:9092 --p2p.laddr tcp://$P2P2 --p2p.persistent_peers $PP1,$PP3 --home $HOME2 > $HOME2/logs.log 2>&1 &" C-m +tmux send-keys -t "$SESSION:0.2" "./build/nobled start --api.enable false --rpc.laddr tcp://127.0.0.1:46657 --rpc.pprof_laddr localhost:6062 --grpc.address localhost:9093 --p2p.laddr tcp://$P2P3 --p2p.persistent_peers $PP1,$PP2 --home $HOME3 > $HOME3/logs.log 2>&1 &" C-m + +# Watch logs +tmux send-keys -t "$SESSION:0.0" "tail -f $HOME1/logs.log" C-m +tmux send-keys -t "$SESSION:0.1" "tail -f $HOME2/logs.log" C-m +tmux send-keys -t "$SESSION:0.2" "tail -f $HOME3/logs.log" C-m + +# bring up session +tmux attach-session -t "$SESSION" + +# To reset: +# `ctr-c` to kill 1 out of the three nodes, +# `killall nobled` to kill the rest of the nodes +# (ctrl-b then d) to exit out of tmux session session +# `tmux kill-session -t 3v-network` to kill tmux session diff --git a/proposal.go b/proposal.go new file mode 100644 index 00000000..d208d879 --- /dev/null +++ b/proposal.go @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 NASD Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package noble + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "time" + + "cosmossdk.io/errors" + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/mempool" + + "connectrpc.com/connect" + jester "jester.noble.xyz/api" + + wormholekeeper "github.com/noble-assets/wormhole/keeper" + wormholetypes "github.com/noble-assets/wormhole/types" + vaautils "github.com/wormhole-foundation/wormhole/sdk/vaa" + + dollarkeeper "dollar.noble.xyz/keeper" + dollarportaltypes "dollar.noble.xyz/types/portal" +) + +// jesterIndex is the index of the injected Jester response in a block. +const jesterIndex = 0 + +type ProposalHandler struct { + jesterClient jester.QueryServiceClient + wormholeServer wormholetypes.QueryServer + dollarPortalServer dollarportaltypes.MsgServer + + defaultPrepareProposalHandler sdk.PrepareProposalHandler + defaultProcessProposalHandler sdk.ProcessProposalHandler + defaultPreBlocker sdk.PreBlocker +} + +func NewProposalHandler( + app *baseapp.BaseApp, + mempool mempool.Mempool, + preBlocker sdk.PreBlocker, + jesterClient jester.QueryServiceClient, + dollarKeeper *dollarkeeper.Keeper, + wormholeKeeper *wormholekeeper.Keeper, +) *ProposalHandler { + defaultHandler := baseapp.NewDefaultProposalHandler(mempool, app) + + return &ProposalHandler{ + jesterClient: jesterClient, + wormholeServer: wormholekeeper.NewQueryServer(wormholeKeeper), + dollarPortalServer: dollarkeeper.NewPortalMsgServer(dollarKeeper), + + defaultPrepareProposalHandler: defaultHandler.PrepareProposalHandler(), + defaultProcessProposalHandler: defaultHandler.ProcessProposalHandler(), + defaultPreBlocker: preBlocker, + } +} + +// PrepareProposal is the logic called by the current block proposer to prepare +// a block proposal. Noble modifies this by making a request to our sidecar +// service, Jester, to check if there are any outstanding $USDN transfers that +// need to be relayed to Noble. These transfers (in the form of Wormhole VAAs) +// are injected as the first transaction of the block, and are later processed +// by the PreBlocker handler. +func (h *ProposalHandler) PrepareProposal() sdk.PrepareProposalHandler { + return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { + logger := ctx.Logger() + + res, err := h.defaultPrepareProposalHandler(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "default PrepareProposal handler failed") + } + + ctxWithTimeout, cancel := context.WithTimeout(ctx, 500*time.Millisecond) + defer cancel() + + request := connect.NewRequest(&jester.GetVoteExtensionRequest{}) + jesterRes, err := h.jesterClient.GetVoteExtension(ctxWithTimeout, request) + if err != nil { + logger.Error("failed to query jester", "err", err) + } + + if jesterRes != nil && jesterRes.Msg != nil && jesterRes.Msg.Dollar != nil && len(jesterRes.Msg.Dollar.Vaas) > 0 { + var nonExecutedVAAs [][]byte + + for _, raw := range jesterRes.Msg.Dollar.Vaas { + vaa, err := vaautils.Unmarshal(raw) + if err != nil { + logger.Warn("failed to unmarshal transfer from jester", "err", err) + continue + } + + wormholeRes, _ := h.wormholeServer.ExecutedVAA(ctx, &wormholetypes.QueryExecutedVAA{ + Input: vaa.SigningDigest().String(), + }) + + if wormholeRes != nil && !wormholeRes.Executed { + nonExecutedVAAs = append(nonExecutedVAAs, raw) + } else { + logger.Warn("skipped already executed transfer from jester", "identifier", vaa.MessageID()) + } + } + + if len(nonExecutedVAAs) > 0 { + jesterRes.Msg.Dollar.Vaas = nonExecutedVAAs + + bz, err := json.Marshal(jesterRes.Msg) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal injected jester tx") + } + res.Txs = slices.Insert(res.Txs, jesterIndex, bz) + + logger.Info(fmt.Sprintf("injected %d pending transfers from jester", len(nonExecutedVAAs))) + } + } + + return &abci.ResponsePrepareProposal{Txs: res.Txs}, nil + } +} + +// ProcessProposal is the logic called by all validators except the current +// block proposer to process a block proposal. Noble modifies this by first +// removing the injected transaction from our sidecar service, Jester, then +// executing the default proposal processing logic provided by the Cosmos SDK. +func (h *ProposalHandler) ProcessProposal() sdk.ProcessProposalHandler { + return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + resAccept := &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT} + resReject := &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} + + if len(req.Txs) == 0 { + return resAccept, nil + } + + if h.isJesterTx(req.Txs[jesterIndex]) { + req.Txs = req.Txs[jesterIndex+1:] + } + + res, err := h.defaultProcessProposalHandler(ctx, req) + if err != nil || (res != nil && !res.IsAccepted()) { + return resReject, errors.Wrap(err, "default ProcessProposal handler failed") + } + + return resAccept, nil + } +} + +// PreBlocker processes all injected $USDN transfers from Jester. +func (h *ProposalHandler) PreBlocker() sdk.PreBlocker { + return func(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) { + res, err := h.defaultPreBlocker(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "default PreBlocker failed") + } + + if len(req.Txs) == 0 { + return res, nil + } + + tx := req.Txs[jesterIndex] + if h.isJesterTx(tx) { + h.handleJesterTx(ctx, tx) + } + + return res, nil + } +} + +// isJesterTx is a utility that returns if a given transaction is from Jester. +func (h *ProposalHandler) isJesterTx(bytes []byte) bool { + var jesterResponse jester.GetVoteExtensionResponse + return json.Unmarshal(bytes, &jesterResponse) == nil +} + +// handleJesterTx is a utility that handles an injected transaction from Jester. +func (h *ProposalHandler) handleJesterTx(ctx sdk.Context, bytes []byte) { + logger := ctx.Logger() + + defer func() { + if r := recover(); r != nil { + logger.Error("recovered panic when handling transfers from jester", "err", r) + } + }() + + var res jester.GetVoteExtensionResponse + if err := json.Unmarshal(bytes, &res); err != nil { + logger.Error("failed to unmarshal injected jester tx", "err", err) + return + } + + if res.Dollar != nil && len(res.Dollar.Vaas) > 0 { + var count int + + for _, raw := range res.Dollar.Vaas { + vaa, err := vaautils.Unmarshal(raw) + if err != nil { + logger.Error("failed to unmarshal transfer from jester", "err", err) + continue + } + + cachedCtx, writeCache := ctx.CacheContext() + _, err = h.dollarPortalServer.Deliver(cachedCtx, &dollarportaltypes.MsgDeliver{ + Vaa: raw, + }) + + if err == nil { + writeCache() + count++ + } else { + logger.Error("failed to process transfer from jester", "identifier", vaa.MessageID(), "err", err) + } + } + + if count > 0 { + logger.Info(fmt.Sprintf("processed %d transfers from jester", count)) + } + } +}