Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,19 @@ provider-start: build-apps
$(providerd) genesis add-genesis-account val 1000000000000uatone,1000000000000uphoton
$(providerd) keys add user
$(providerd) genesis add-genesis-account user 1000000000uatone
# Fund the IBC relayer account so it can pay fees to create the client on the provider
@chmod +x ./scripts/add-relayer-key.sh
@./scripts/add-relayer-key.sh ./build/provider $(provider_home)
$(providerd) genesis add-genesis-account relayer 100000000uatone
$(providerd) genesis add-genesis-account relayer 1000000000uatone
@./scripts/add-owner-key.sh ./build/provider $(provider_home)
$(providerd) genesis add-genesis-account owner 100000000uatone
$(providerd) genesis gentx val 1000000000uatone
$(providerd) genesis collect-gentxs

# Set validator gas prices
sed -i.bak 's#^minimum-gas-prices = .*#minimum-gas-prices = "0.01uatone,0.01uphoton"#g' $(provider_home)/config/app.toml
# Bind the RPC to all interfaces so the containerized ts-relayer can reach it
sed -i.bak 's#tcp://127.0.0.1:26657#tcp://0.0.0.0:26657#g' $(provider_home)/config/config.toml
# enable REST API
$(providerd) config set app api.enable true
# Decrease voting period to 5min
Expand Down Expand Up @@ -122,8 +125,10 @@ consumer-init: build-apps
$(consumerd) genesis add-genesis-account owner 100000000uatone
# Set gas prices
sed -i.bak 's#^minimum-gas-prices = .*#minimum-gas-prices = "0.01uatone,0.01uphoton"#g' $(consumer_home)/config/app.toml
# Use different ports to avoid conflicts with provider
sed -i.bak 's#tcp://127.0.0.1:26657#tcp://127.0.0.1:26667#g' $(consumer_home)/config/config.toml
# Use different ports to avoid conflicts with provider. Bind the RPC to
# 0.0.0.0 (not just loopback) so the containerized ts-relayer can dial it
# via host.docker.internal.
sed -i.bak 's#tcp://127.0.0.1:26657#tcp://0.0.0.0:26667#g' $(consumer_home)/config/config.toml
sed -i.bak 's#tcp://0.0.0.0:26656#tcp://0.0.0.0:26666#g' $(consumer_home)/config/config.toml
sed -i.bak 's#tcp://127.0.0.1:26658#tcp://127.0.0.1:26668#g' $(consumer_home)/config/config.toml
sed -i.bak 's#tcp://localhost:26657#tcp://localhost:26667#g' $(consumer_home)/config/client.toml
Expand All @@ -136,7 +141,7 @@ consumer-init: build-apps
consumer-create:
@echo "Creating consumer chain on provider..."
@mkdir -p /tmp/vaas-test
@echo '{"chain_id": "consumer-localnet", "metadata": {"name": "consumer", "description": "test consumer chain", "metadata": "{}"}, "initialization_parameters": {"initial_height": {"revision_number": 0, "revision_height": 1}, "genesis_hash": "", "binary_hash": "", "spawn_time": "2024-01-01T00:00:00Z", "unbonding_period": 1728000000000000, "vaas_timeout_period": 2419200000000000, "historical_entries": 10000, "connection_id": ""}}' > /tmp/vaas-test/create_consumer.json
@echo '{"chain_id": "consumer-localnet", "metadata": {"name": "consumer", "description": "test consumer chain", "metadata": "{}"}, "initialization_parameters": {"initial_height": {"revision_number": 0, "revision_height": 1}, "genesis_hash": "", "binary_hash": "", "spawn_time": "2024-01-01T00:00:00Z", "unbonding_period": 1728000000000000, "vaas_timeout_period": 3600000000000, "safe_mode_threshold": 10800000000000, "historical_entries": 10000}}' > /tmp/vaas-test/create_consumer.json
# Submitted by the owner key (HD index 1 of the shared mnemonic, present on
# both chains): the submitter becomes the consumer's owner, and the owner
# also pins the provider client on the consumer side. A dedicated key,
Expand Down Expand Up @@ -201,7 +206,8 @@ TS_RELAYER ?= ghcr.io/allinbits/ibc-v2-ts-relayer:latest
ts-relayer-start:
@echo "Starting ts-relayer..."
@docker rm -f vaas-ts-relayer 2>/dev/null || true
@docker run -d --name vaas-ts-relayer --network host \
@docker run -d --name vaas-ts-relayer \
--add-host=host.docker.internal:host-gateway \
--cap-add IPC_LOCK \
$(TS_RELAYER)
@sleep 3
Expand All @@ -220,8 +226,8 @@ ts-relayer-start:
@docker exec vaas-ts-relayer /bin/with_keyring ibc-v2-ts-relayer add-path \
-s provider-localnet \
-d consumer-localnet \
--surl http://127.0.0.1:26657 \
--durl http://127.0.0.1:26667 \
--surl http://host.docker.internal:26657 \
--durl http://host.docker.internal:26667 \
--ibc-version 2
@echo "ts-relayer configured and running (log: /tmp/vaas-ts-relayer.log)"

Expand Down
6 changes: 2 additions & 4 deletions app/consumer/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,10 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []str
}
}

// GetValidatorSet returns a slice of bonded validators.
// GetValidatorSet returns the consumer's cross-chain validators. The set may be
// empty if the consumer has not yet received a validator set from the provider.
func (app *App) GetValidatorSet(ctx sdk.Context) ([]tmtypes.GenesisValidator, error) {
cVals := app.ConsumerKeeper.GetAllCCValidator(ctx)
if len(cVals) == 0 {
return nil, fmt.Errorf("empty validator set")
}

vals := []tmtypes.GenesisValidator{}
for _, v := range cVals {
Expand Down
26 changes: 26 additions & 0 deletions app/consumer/export_empty_valset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package app

import (
"testing"

"github.com/stretchr/testify/require"

cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"

"cosmossdk.io/log"

dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/testutil/sims"
)

// TestGetValidatorSetToleratesEmptySet: a consumer that has not yet received a
// validator set from the provider has no cross-chain validators, and exporting
// it must succeed with an empty set rather than fail.
func TestGetValidatorSetToleratesEmptySet(t *testing.T) {
app := New(log.NewNopLogger(), dbm.NewMemDB(), nil, true, sims.EmptyAppOptions{})
ctx := app.NewContextLegacy(true, cmtproto.Header{Height: app.LastBlockHeight()})

vals, err := app.GetValidatorSet(ctx)
require.NoError(t, err, "exporting a consumer with no validators must not fail")
require.Empty(t, vals)
}
8 changes: 5 additions & 3 deletions docs/consumer-liveness.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,15 @@ safe mode engages before the provider's sweep removes it.

## 6. Operator guidance: the liveness query

Operators can observe a consumer's liveness without waiting for removal through the
provider's `QueryConsumerLiveness` gRPC query, exposed over REST as:
Operators can observe a consumer's liveness without waiting for removal
through the provider's `QueryConsumerLiveness` query, via the CLI:

```
/vaas/provider/consumer_liveness/<consumer-id>
providerd query provider consumer-liveness <consumer-id>
```

or over REST as `/vaas/provider/consumer_liveness/<consumer-id>`.

It returns one stored value and three derived from it:

- `last_ack_time` -- the stored block time of the consumer's last successful VSC ack.
Expand Down
15 changes: 12 additions & 3 deletions tests/e2e/e2e_debt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func (s *baseTestSuite) consumerUserBech32() string {
// debt gate fires here just like it would for a real broadcast. It carries no
// fee, keeping it neutral to the photon-only policy (simulation permits empty
// fees) so the main suite, which enables that policy, can use it too. Returns
// the stderr output so callers can inspect rejection reasons.
// the stderr output so callers can inspect rejection reasons; a successful
// simulation prints "gas estimate: <n>" there, which is the only reliable
// success marker (the exec helper does not surface exit codes as errors).
func (s *baseTestSuite) consumerBankSendDryRun() (string, error) {
user := s.consumerUserBech32()
_, stderr, err := s.dockerExec(s.consumerValRes[0].Container.ID, []string{
Expand Down Expand Up @@ -170,7 +172,10 @@ func (s *IntegrationTestSuite) testConsumerDebtFlow() {
s.T().Log("waiting for consumer to enter debt (bank send should be rejected)...")
s.Require().Eventuallyf(func() bool {
out, err := s.consumerBankSendDryRun()
return err != nil || strings.Contains(out, "consumer chain is in debt")
if err != nil {
return false
}
return strings.Contains(out, "consumer chain is in debt")
}, 2*time.Minute, 5*time.Second,
"consumer did not enter debt; last dry-run did not surface debt error")

Expand All @@ -185,7 +190,11 @@ func (s *IntegrationTestSuite) testConsumerDebtFlow() {
if err != nil {
return false
}
return !strings.Contains(out, "consumer chain is in debt")
// The simulation must actually succeed, not merely fail for some
// other reason: a broken CLI or an unreachable node also stops
// producing the debt error, and only the gas estimate proves the
// ante chain admitted the tx.
return strings.Contains(out, "gas estimate")
}, 2*time.Minute, 5*time.Second,
"consumer did not exit debt after fee pool was funded")
})
Expand Down
13 changes: 13 additions & 0 deletions tests/e2e/e2e_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ func (s *baseTestSuite) dockerExec(containerID string, cmd []string) (bytes.Buff
return stdout, stderr, fmt.Errorf("failed to start exec: %w", err)
}

// StartExec reports how the exec itself went, not how the command inside it
// exited, so a command that failed comes back with err == nil and an empty
// stdout. Callers that unmarshal stdout then fail on the empty document
// rather than on the reason -- a CLI route that no longer exists, a bad
// flag, an unfunded key -- which is invisible unless the exit code and
// stderr are surfaced. Log them and leave the error nil: several callers
// deliberately run commands expected to fail and assert on stderr
// themselves.
if inspect, inspectErr := s.dkrPool.Client.InspectExec(exec.ID); inspectErr == nil && inspect.ExitCode != 0 {
s.T().Logf("tolerated nonzero exit %d (callers assert on stderr; often an expected rejection or a poll miss): %v\nstdout: %s\nstderr: %s",
inspect.ExitCode, cmd, stdout.String(), stderr.String())
}

return stdout, stderr, nil
}

Expand Down
14 changes: 9 additions & 5 deletions tests/e2e/e2e_liveness_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,9 @@ func (s *LivenessIntegrationTestSuite) testRealSafeMode() {
if err != nil {
return false
}
return !strings.Contains(out, "consumer chain is in debt") &&
!strings.Contains(out, "stale validator set")
// Only a successful simulation (gas estimate) proves normal mode;
// any other failure would otherwise pass for "no gate error".
return strings.Contains(out, "gas estimate")
}, 2*time.Minute, 5*time.Second,
"consumer did not enter normal mode after fee pool funding")

Expand All @@ -391,7 +392,10 @@ func (s *LivenessIntegrationTestSuite) testRealSafeMode() {
s.T().Log("waiting for consumer to enter restricted mode (VSC stale after ~5s)...")
s.Require().Eventuallyf(func() bool {
out, err := s.consumerBankSendDryRun()
return err != nil || strings.Contains(out, "stale validator set") ||
if err != nil {
return false
}
return strings.Contains(out, "stale validator set") ||
strings.Contains(out, "consumer chain is in debt")
}, 2*time.Minute, 3*time.Second,
"consumer did not enter restricted mode after VSC staleness")
Expand All @@ -409,8 +413,8 @@ func (s *LivenessIntegrationTestSuite) testRealSafeMode() {
if err != nil {
return false
}
return !strings.Contains(out, "stale validator set") &&
!strings.Contains(out, "consumer chain is in debt")
// As above: only the gas estimate proves the gate reopened.
return strings.Contains(out, "gas estimate")
}, 3*time.Minute, 5*time.Second,
"consumer did not exit restricted mode after VSC delivery resumed")
s.T().Log("consumer back in normal mode after VSC delivery")
Expand Down
42 changes: 39 additions & 3 deletions x/vaas/provider/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func NewQueryCmd() *cobra.Command {
cmd.AddCommand(CmdAllConsumerFeesPerBlockOverrides())
cmd.AddCommand(CmdConsumerFeePoolClaim())
cmd.AddCommand(CmdConsumerFeePoolClaims())
cmd.AddCommand(CmdConsumerLiveness())
cmd.AddCommand(CmdPendingDowntimeSlashes())
cmd.AddCommand(CmdWithheldFeeRecords())
return cmd
Expand Down Expand Up @@ -135,7 +136,6 @@ func CmdConsumerChains() *cobra.Command {
return cmd
}

// TODO: fix naming
func CmdConsumerValidatorKeyAssignment() *cobra.Command {
bech32PrefixConsAddr := sdk.GetConfig().GetBech32ConsensusAddrPrefix()
cmd := &cobra.Command{
Expand Down Expand Up @@ -186,7 +186,6 @@ $ %s query provider validator-consumer-key 3 %s1gghjut3ccd8ay0zduzj64hwre2fxs9ld
return cmd
}

// TODO: fix naming
func CmdProviderValidatorKey() *cobra.Command {
bech32PrefixConsAddr := sdk.GetConfig().GetBech32ConsensusAddrPrefix()
cmd := &cobra.Command{
Expand Down Expand Up @@ -571,7 +570,11 @@ func CmdConsumerFeePoolClaims() *cobra.Command {
if err != nil {
return err
}
pageReq, err := client.ReadPageRequest(cmd.Flags())
fs, err := client.FlagSetWithPageKeyDecoded(cmd.Flags())
if err != nil {
return err
}
pageReq, err := client.ReadPageRequest(fs)
if err != nil {
return err
}
Expand All @@ -591,6 +594,39 @@ func CmdConsumerFeePoolClaims() *cobra.Command {
return cmd
}

// CmdConsumerLiveness queries the liveness status of a consumer chain: the last
// VSC-ack time, the grace period, the removal ETA, and whether it is degraded.
func CmdConsumerLiveness() *cobra.Command {
cmd := &cobra.Command{
Use: "consumer-liveness [consumer-id]",
Short: "Query the liveness status of a consumer chain",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

consumerId, err := parseConsumerIdArg(args[0])
if err != nil {
return err
}
req := &types.QueryConsumerLivenessRequest{ConsumerId: consumerId}
res, err := queryClient.QueryConsumerLiveness(cmd.Context(), req)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}

// CmdPendingDowntimeSlashes queries the pending downtime slashes queued for a
// consumer, awaiting the challenge window before execution.
func CmdPendingDowntimeSlashes() *cobra.Command {
Expand Down
6 changes: 4 additions & 2 deletions x/vaas/provider/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ where create_consumer.json has the following structure:
"binary_hash": "",
"spawn_time": "2024-08-29T12:26:16.529913Z",
"unbonding_period": 1728000000000000,
"ccv_timeout_period": 2419200000000000,
"vaas_timeout_period": 3600000000000,
"safe_mode_threshold": 10800000000000,
"historical_entries": 10000
}
}
Expand Down Expand Up @@ -348,7 +349,8 @@ where update_consumer.json has the following structure:
"binary_hash": "",
"spawn_time": "2024-08-29T12:26:16.529913Z",
"unbonding_period": 1728000000000000,
"ccv_timeout_period": 2419200000000000,
"vaas_timeout_period": 3600000000000,
"safe_mode_threshold": 10800000000000,
"historical_entries": 10000
},
"new_chain_id": "newConsumer-1", // is optional and can be empty (i.e., "new_chain_id": "")
Expand Down
Loading