Undelegate problem

Hi! We are implementing the Staking mechanism for Cosmos SDK chains, when we want to create MsgDelegate we create 2 operations:

  • 1st removes the staked balance from Delegator.
  • 2nd is an operation that the AccountIdentifier is the Validator and the amount is the delegated amount.

This technically is not correct, since the amount we delegate goes to an internal module account, but based on this operations looks like we are moving balance from the delegator to the validator which is not true.

How to approach this first message?

Next one is Undelegate:

The problem here is similar to the other one, we don’t move balance from the validator to the delegator, plus we have another problem that is that the amount that is undelegated is not reflected in the account of the delegator until 21 days. But that happens automatically, there is no trasaction that sends balance 21 days later.

How to approach this case?

And the last message is MsgWithdrawDelegatorRewards:

That a delegator can send to get the rewards that were cumulated. But we don’t specify any amount, so you just get the rewards. So we cannot do Parse of this kind of message, because the amount is not specified in the Transaction.

How can we approach this last one?

Hi, I worked on the Oasis Rosetta backend.

We use a Rosetta subaccount to keep the tokens delegated to a validator separate from the validator’s liquid tokens.

For this, we relied on Rosetta allowing transactions that belong to the block itself.

We had something similar in Oasis where undelegating is done by a number of shares and not by a token amount. In our implementation, we left the amount blank in the intent operation sequence and had it fill in the amount after it gets processed on chain.

See our specs here https://github.com/oasisprotocol/oasis-core-rosetta-gateway#staking-add-escrow and here https://github.com/oasisprotocol/oasis-core-rosetta-gateway#staking-reclaim-escrow

1 Like

:wave: @jgimeno!

Great question! The most “Rosetta-friendly” way to model a staking operation like this is to have a decrease operation from the delegator’s “liquid” balance (as you’ve already indicated you are doing) and then an increase to a SubAccount representing the staked balance. This would look something like this transaction:

{
  "transaction_identifier": {
    "hash": "tx1"
  },
  "operations": [
    {
      "type": "delegate",
      "account_identifier": {
        "address": "a"
      },
      "amount": {
        "value": "-100",
        "currency": {
          "symbol": "ATOM",
          "decimals": 8
        }
      }
    },
    {
      "type": "delegate",
      "account_identifier": {
        "address": "a",
        "sub_account": {
          "address": "staked_balance",
          "metadata": {
            "validator": "validator_address"
          }
        }
      },
      "amount": {
        "value": "100",
        "currency": {
          "symbol": "ATOM",
          "decimals": 8
        }
      }
    }
  ]
}

You could either store the validator address as the SubAccount.Address or use a “state” (like staked_balance) and store the validator address in SubAccount.Metadata. Using a “state” may provide a little more readability.

We consider Cosmos undelegation to be discrete actions: BEGIN_UNDELEGATION and COMPLETE_UNDELEGATION. We recommend that the BEGIN_UNDELEGATION action appear in the block where the undelegation starts. This transaction would look something like:

{
  "transaction_identifier": {
    "hash": "tx1"
  },
  "operations": [
    {
      "type": "begin_undelegation",
      "account_identifier": {
        "address": "a",
        "sub_account": {
          "address": "staked_balance",
          "metadata": {
            "validator": "validator_address"
          }
        }
      },
      "amount": {
        "value": "-100",
        "currency": {
          "symbol": "ATOM",
          "decimals": 8
        }
      }
    }
  ]
}

For COMPLETE_UNDELEGATION, we recommend using a “block transaction” (a transaction in the block where the transaction hash is equal to the block hash) in the block where undelegation completes. This is because, as you mentioned, the COMPLETE_UNDELEGATION step is not attributable to a transaction hash but rather a block event. You can read more about operations that are attributable to a block here:

We’ve had luck parsing COMPLETE_UNDELEGATIONS by looking for "github.com/cosmos/cosmos-sdk/x/staking/types". EventTypeCompleteUnbonding events in the Tendermint (on version 0.32.10) "github.com/tendermint/tendermint/rpc/core/types". ResultBlockResults.Results.EndBlock.Events array. The object returned here should contain all information necessary to create a block transaction that looks like this:

{
  "transaction_identifier": {
    "hash": "block_tx"
  },
  "operations": [
    {
      "type": "complete_undelegation",
      "account_identifier": {
        "address": "a"
      },
      "amount": {
        "value": "100",
        "currency": {
          "symbol": "ATOM",
          "decimals": 8
        }
      },
      "metadata": {
        "validator": "validator_address"
      }
    }
  ]
}

We recommend including the validator address in the operation metadata so that the caller understands where the undelegation completion came from.

This is a complicated one (as you’ve mentioned) because the Construction API tries to match the intent provided with what is seen on-chain (comparing AccountIdentifier, Type, and Amount). There are 2 possible solutions here:

  1. Break the reward withdrawal into 2 operations where the first operation is something like begin_reward_withdrawal where an amount is not specified. On-chain, this transaction would include another operation of type complete_reward_withdrawal with a populated amount (modeling the actual reward withdrawal more as an event/on-chain execution result). This would allow parse to pass because the first operation would satisfy our correctness check. The intent would look like this:
[
  {
    "type": "begin_reward_withdrawal",
    "account_identifier": {
      "address": "a",
      "sub_account": {
        "address": "staked_balance",
        "metadata": {
          "validator": "validator_address"
        }
      }
    }
  }
]

The on-chain transaction would look like this:

{
  "transaction_identifier": {
    "hash": "tx1"
  },
  "operations": [
    {
      "type": "begin_reward_withdrawal",
      "account_identifier": {
        "address": "a",
        "sub_account": {
          "address": "staked_balance",
          "metadata": {
            "validator": "validator_address"
          }
        }
      }
    },
    {
      "type": "complete_reward_withdrawal",
      "account_identifier": {
        "address": "a"
      },
      "amount": {
        "value": "100",
        "currency": {
          "symbol": "ATOM",
          "decimals": 8
        }
      }
    }
  ]
}
  1. Modify rosetta-specifications, rosetta-sdk-go, and rosetta-cli to allow matching operations that have different amounts (based on some additional key added to the Operation model). TL;DR This is equivalent to adding “wildcard” support for amount matching.

We’d prefer option 1 because the changes required to make option 2 work are more invasive/touch many repos (both internal and external on our side). If you feel strongly about option 2, let us know and we can discuss more.

Yep! Totally agree here. See the example we added in the initial reply for what we mean by this.

Agree here as well! You can see the link we posted where we discuss “block transactions” and when they are useful.

Curious if your modeling strategy allowed you to pass rosetta-cli check:construction (particularly interested in the part where you populate the intent operation…seems like this would cause a parse mismatch error). This wouldn’t affect rosetta-cli check:data checks.

If so, would you mind sharing your rosetta-cli configuration file where you test this!

Haven’t tried that. We only have check:construction running on transfers. I’ll bring this up with our team. Thanks

1 Like