Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redeem rewards from another wallet #1967

Merged
merged 11 commits into from
Jul 31, 2020
Merged

Redeem rewards from another wallet #1967

merged 11 commits into from
Jul 31, 2020

Conversation

KtorZ
Copy link
Member

@KtorZ KtorZ commented Jul 28, 2020

Issue Number

#1965

Overview

  • 8f97407
    📍 let the caller decide how to generate a reward account for signing withdrawals
    The goal is to allow a mnemonic to be passed around as a function
    argument. If it is, then that account from the mnemonic is used.
    Otherwise, it will defaults to the wallet's own account.

  • e26fc38
    📍 make it possible to query rewards of any arbitrary reward account
    Yet, the server still query rewards from standard wallet reward
    account at the moment. Next commits will make it possible for users to
    pass a mnemonic to the API.

  • 657e541
    📍 remove duplicated logic between 'readNextWithdrawal' and 'queryRewardBalance'
    This partially revert some of the previous commit, but that's for the best.

  • fc14a3b
    📍 extend 'POST' transaction Shelley endpoint to support redeeming rewards
    So, now the transaction endpoint accepts two kind of payloads, either a list of payments like before, or simply a source mnemonic from which rewards would be drained / withdrawn.

  • 9161f9f
    📍 update 'postTransaction' handler to handle payments and withdrawals

  • 7a71a7e
    📍 separate 'toChimericAccount' from 'HasRewardAccount' type-class
    This makes it slightly easier to deal with all these constraints in the API, in particular in the context of withdrawals.
    Also, I've completely removed the 'HasRewardAccount' constraint from the 'readChimericAccount' function. Instead, that function will
    dynamically check that the type is a Shelley type and fails with a proper error otherwise.

  • 04c96ea
    📍 actually get rid of 'HasRewardAccount' type-class altogether
    This type class only existed in order to be able to get a reward account from a sequential state to assess whether an account was ours or not.
    We had to defined an unsound 'HasRewardAccount' on 'IcarusKey' because of that and carry this instance in many places in the API. Instead, we
    can simply carry a 'IsOurs _ ChimericAccount' constraints up to API instantiation.

  • 03da476
    📍 unify withdrawal requests frmo self or external wallets under one common API
    This is so-to-speak a breaking change compared to what we just released yet, it's for the best. Now there's one consistent way of creating withdrawals! This also gets rid of a few edge-case that are not possible anymore by design.

  • 3e0e9db
    📍 add pre-funded wallets with rewards using MIR certificates to the integration cluster

  • 3183a7f
    📍 add integration scenarios for reward redemption
    Trying various scenarios, from self and from other wallets.

  • d3ebc72
    📍 prevent users from withdrawing from empty reward accounts.

Comments

Screenshot from 2020-07-29 23-38-52 Screenshot from 2020-07-29 23-39-05

@KtorZ KtorZ requested a review from a team July 28, 2020 02:25
@KtorZ KtorZ self-assigned this Jul 28, 2020
<*> fmap getQuantity (readDelegationRewardBalance pk)
-> Either WalletId SomeMnemonic
-> ExceptT ErrFetchRewards IO (Quantity "lovelace" Word64)
readNextWithdrawal ctx wid src = db & \DBLayer{..} -> do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src was unexpected name for me. tbh. maybe acctSrc, chimericOrigin ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. My creativity for names at 4 a.m. was somewhat diminished 😂

Left (ErrGetAccountBalanceNetworkUnreachable e) ->
Left $ ErrFetchRewardsNetworkUnreachable e

fromOwnWallet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

fromOwnWallet
:: WalletId
-> ExceptT ErrFetchRewards IO ChimericAccount
fromOwnWallet _wid = db & \DBLayer{..} -> do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -159,6 +165,25 @@ instance Functor m => Functor (NetworkLayer m target) where
{ nextBlocks = fmap (fmap f) . nextBlocks nl
}

-- Fetching the account balance may sometimes fail, especially on epoch
-- boundaries. Therefore, we add some retrying mecanism here to cope with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- boundaries. Therefore, we add some retrying mecanism here to cope with
-- boundaries. Therefore, we add some retrying mechanism here to cope with

-> ChimericAccount
-> ExceptT ErrGetAccountBalance IO (Quantity "lovelace" Word64)
getAccountBalanceRetry nl account = ExceptT $
retrying policy decision (const $ runExceptT $ getAccountBalance nl account)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume here, that there will be a moment, where cardano-node will be able to discover itn rewards basing on chimeric account? so itn and shelley chimeric account will be married, the same format, etc ? and we can reuse the same local query infrastructure already present?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. In principle, on the 3rd of August, rewards will be transferred to the Mainnet based on the same public key that was used on the ITN.

@paweljakubas
Copy link
Contributor

so far so good (although I liked more fromOwnWallet and fromExternalWallet and they changed in last commit ;-)
of course, waiting for proof of concept in integration testing (the question is whether itn has been absorbed already in cardano-node and it is possible now )

@@ -491,10 +499,27 @@ data PostTransactionData (n :: NetworkDiscriminant) = PostTransactionData
, passphrase :: !(ApiT (Passphrase "lenient"))
} deriving (Eq, Generic, Show)

data PostWithdrawalData = PostWithdrawalData
{ source :: !(ApiMnemonicT '[15,18,21,24])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not to use mnemonicSentence like in other places?

} deriving (Eq, Generic, Show)

newtype PostWithdrawalFeeData = PostWithdrawalFeeData
{ source :: ApiMnemonicT '[15,18,21,24]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here also, why not to use mnemonicSentence like in other places?

instance DecodeAddress t => FromJSON (PostPaymentOrWithdrawalData t) where
parseJSON val = flip (withObject "PostPaymentOrWithdrawalData") val $ \o -> do
case (HM.lookup "source" o, HM.lookup "payments" o) of
(Just{}, Just{}) -> empty
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

toJSON (PostPaymentOrWithdrawalFeeData (Left x)) = toJSON x
toJSON (PostPaymentOrWithdrawalFeeData (Right x)) = toJSON x
instance DecodeAddress t => FromJSON (PostPaymentOrWithdrawalFeeData t) where
parseJSON val = flip (withObject "PostPaymentOrWithdrawalFeeData") val $ \o -> do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about reusing what was done in impl of instance DecodeAddress t => FromJSON (PostPaymentOrWithdrawalData t) here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the JSON object isn't exactly the same (FeeData has no passphrase).

=> ([(Tx, TxMeta)], UTxO)
-> Tx
-> State s ([(Tx, TxMeta)], UTxO)
applyTx (!txs, !u) tx = do
ourU <- state $ utxoOurs tx
let ourIns = Set.fromList (inputs tx) `Set.intersection` dom (u <> ourU)
let u' = (u <> ourU) `excluding` ourIns
ourWithdrawals <- mapMaybeM ourChimericAccount $ Map.toList $ withdrawals tx
let wdrls = fromIntegral . getCoin <$> Map.elems (withdrawals tx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Since we can now have withdrawals in our wallet that aren't from our chimeric account (but will still contribute positively to the input balance of a transaction), we need to account for pretty much all withdrawals in transactions (or otherwise, more complicated, "remember" all chimeric account we've made withdrawals from...).

This will work fine until we introduce multisig (where some inputs will not be ours... right now, if one input is ours, then they all are so the problem is quite simplified). By then, we'll have to review this.

ApiPostWithdrawalData: &ApiPostWithdrawalData
type: object
required:
- source
Copy link
Contributor

@paweljakubas paweljakubas Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use - mnemonic_sentence all around for this bit

@KtorZ KtorZ force-pushed the KtorZ/1965/itn-withdrawals branch from 30f2d38 to 7c8289c Compare July 29, 2020 21:57
@KtorZ KtorZ added the ADDING FEATURE Mark a PR as adding a new feature, for auto-generated CHANGELOG label Jul 29, 2020
@KtorZ KtorZ marked this pull request as ready for review July 29, 2020 22:06
KtorZ added 11 commits July 31, 2020 00:38
…thdrawals

  The goal is to allow a mnemonic to be passed around as a function
  argument. If it is, then that account from the mnemonic is used.
  Otherwise, it will defaults to the wallet's own account.
  Yet, the server still query rewards from standard wallet reward
  account at the moment. Next commits will make it possible for users to
  pass a mnemonic to the API.
…Balance'

  This partially revert some of the previous commit, but that's for the best.
  So, now the transaction endpoint accepts two kind of payloads, either a list of payments like before, or simply a source mnemonic from which rewards would be drained / withdrawn.
  This makes it slightly easier to deal with all these constraints in the API, in particular in the context of withdrawals.
  Also, I've completely removed the 'HasRewardAccount' constraint from the 'readChimericAccount' function. Instead, that function will
  dynamically check that the type is a Shelley type and fails with a proper error otherwise.
  This type class only existed in order to be able to get a reward account from a sequential state to assess whether an account was ours or not.
  We had to defined an unsound 'HasRewardAccount' on 'IcarusKey' because of that and carry this instance in many places in the API. Instead, we
  can simply carry a 'IsOurs _ ChimericAccount' constraints up to API instantiation.
…mon API

  This is so-to-speak a breaking change compared to what we just released yet, it's for the best. Now there's one consistent way of creating withdrawals! This also gets rid of a few edge-case that are not possible anymore by design.
  Trying various scenarios, from self and from other wallets.
@KtorZ KtorZ force-pushed the KtorZ/1965/itn-withdrawals branch from 7c8289c to d3ebc72 Compare July 30, 2020 22:39
@@ -1240,6 +1254,148 @@ spec = do
[ expectResponseCode @IO HTTP.status404
, expectErrorMessage (errMsg404NoWallet wid)
]

it "SHELLEY_TX_REDEEM_01 - Can redeem rewards from self" $ \ctx -> do
(wSrc,_) <- rewardWallet ctx
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KtorZ
Copy link
Member Author

KtorZ commented Jul 30, 2020

bors try

iohk-bors bot added a commit that referenced this pull request Jul 30, 2020
@iohk-bors
Copy link
Contributor

iohk-bors bot commented Jul 30, 2020

try

Build succeeded


initFaucet :: IO Faucet
initFaucet = Faucet
<$> newMVar seqMnemonics
<*> newMVar icaMnemonics
<*> newMVar rndMnemonics
<*> newMVar mirMnemonics
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generated 10 of them, used 2 or 3 I think in the scenarios I wrote, so there is still some room for some :p

@@ -785,6 +786,27 @@ emptyWalletWith ctx (name, passphrase, addrPoolGap) = do
expectResponseCode @IO HTTP.status201 r
return (getFromResponse id r)

rewardWallet :: Context t -> IO (ApiWallet, Mnemonic 24)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not to call it readMIRwallet to better reflect its nature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was shared between calling everything MIR (which is sort of cardano-node specific terminology) or simply Reward (which is more generic).

I ended up referring to "MIR" in the setup code, because it makes it clearer to what it connects behind the scene, but resorted to "reward" for everything in the DSL and scenarios. That the rewards come from MIR certificates is sort of an implementation detail.

"passphrase": #{fixturePassphrase}
}|]

-- Withdraw rewards from the other wallet.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

(`shouldBe` Quantity 0)
]

-- Try withdrawing AGAIN, rewards that aren't there anymore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

, expectErrorMessage errMsg403WithdrawalNotWorth
]

it "SHELLEY_TX_REDEEM_05 - Can't redeem rewards using byron wallet" $ \ctx -> do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

minFee policy (mempty { withdrawal })
-
minFee policy mempty

pure $ if toInteger withdrawal < 2 * costOfWithdrawal
in
if toInteger withdrawal < 2 * costOfWithdrawal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you recall me why we have here 2 * costOfWithdrawal , ie., why 2 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale was that: "withdrawal should bring at least much value as they cost.".

This is to prevent withdrawals from either loosing money, or being consumed too early when they're really small (imagine an account with 0.31 Ada, and the withdrawal cost 0.3 Ada. If we were just ensuring the withdrawal to be > costOfWithdrawal (without 2 *), then the resulting withdrawal would be 0.01 Ada :/ .. And it really feels not worth it given the fees. Hence the times two.

ErrWithdrawalNotWorth ->
apiError err403 WithdrawalNotWorth $ mconcat
[ "I've noticed that you're requesting a withdrawal from an "
, "account that is either empty or doesn't have a balance big "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it handles also situations, where we got already rewards... saying that account is empty covers this. We don't want to say explicitly something like : presumably the account was already withdrawn ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, an account could be empty for two reasons: either it hasn't received rewards yet, or it has already withdrawn them. I didn't feel like emphasizing on any of these aspects but we could indeed.

@@ -315,6 +318,12 @@ class HardDerivation key => SoftDerivation (key :: Depth -> * -> *) where
-> Index 'Soft 'AddressK
-> key 'AddressK XPub

-- | Derivation of a reward account, as a type-class because different between
-- key types (in particular, Jörmungandr vs Shelley).
class ToChimericAccount k where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be reflected in cardano-addresses ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cardano-addresses doesn't actually treat with chimeric account or stake address at all at the moment. We should eventually though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I was saying about that. We need to remember this in the context of cardano-addresses

@@ -40,7 +40,7 @@ genDelegs:
8ae01cab15f6235958b1147e979987bbdb90788f7c4e185f1632427a:
delegate: b7bf59bb963aa785afe220f5b0d3deb826fd0bcaeeee58cb81ab443d
vrf: 4ebcf8b4c13c24d89144d72f544d1c425b4a3aa1ace30af4eb72752e75b40d3e
updateQuorum: 5
updateQuorum: 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain why this change was needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Took me some time to figure that out (still dreaming about a documentation of the genesis parameters one day 🙏).
MIR certificates are used to transfer rewards to pre-registered stake key. This is not an operation that anyone can do (that would be sort of problematic 😅), but only the core delegates can (same goes for other "governance" operations like updating the protocol parameters).

This updateQuorum parameters controls how many genesis delegate signatures are required for a governance type of request to be considered valid on-chain. Since we only have a single BFT leader in this setup, well, we can only have at most one signature! Thus I had to tweak this so that our BFT leader could act in full dictatorial mode :)

arbitrary = PostTransactionData
<$> arbitrary
<*> arbitrary
<*> elements [Just SelfWithdrawal, Nothing]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't want to include external withdrawals here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit tricky because the swagger2 Haskell libraries doesn't support the oneOf constructor :( ... And here, the swagger schema is a oneOf between a string and a mnemonic sentence; so I had to pick one to not overly complexifies things here; since the mnemonic sentences schema and JSON are already well-tested through other API types, I opted for the self withdrawal one which is completely new and only exists in this context.

Copy link
Contributor

@paweljakubas paweljakubas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very persuasive test scenarios! I like refactoring of ApiPostTransactionData and how posting tx api changed, ie that withdrawal was embedded in PostTransactionData***. we have breaking change, but I believe it is worth it

@KtorZ KtorZ merged commit 5af6e35 into master Jul 31, 2020
@KtorZ KtorZ deleted the KtorZ/1965/itn-withdrawals branch July 31, 2020 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ADDING FEATURE Mark a PR as adding a new feature, for auto-generated CHANGELOG
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants