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

More integration tests #103

Merged
merged 32 commits into from
Sep 23, 2022
Merged

More integration tests #103

merged 32 commits into from
Sep 23, 2022

Conversation

abright
Copy link
Contributor

@abright abright commented Sep 12, 2022

More tests that build on #95

Adds a configurable test actor for testing different scenarios such as those outlined in #80

Single actor tests implemented:

  • Mint to A who rejects hook
  • Mint to token itself (rejected)
  • Mint to A, hook burns the tokens
  • A transfers to token itself (should be rejected)
  • A transfers to A (zero, non-zero)
  • A transfers to A and rejects hook

Multi-actor tests implemented:

  • A transfers to B (zero, non-zero, accept, reject)
  • Mint to A, hook transfers to B
  • Mint to A, hook transfers to B who rejects (but A keeps the tokens)
  • A transfer to B, hook transfer to C
  • A transfer to B, hook transfer to C, hook burns

Cases from #80 still not implemented:

  • A transfer to B, hook transfer to C (rejected), B hook then transfers to D
  • A transfer to B, hook transfer back to A (accept / reject)
  • A transfer to B, hook burns but then aborts (tokens don't move)
  • A transfer to B, hook transfer to C, but then B hook aborts (no move)

Tests are combined into two files (single- and multi-actor) as a compromise between readability and CI build/run time.

There's also a mechanism to reload the current state from a Cid which is used in Mint, Transfer and TransferFrom to update state and then the return value if the state has changed since calling the receiver hook. This covers cases such as transferring or burning from within the receiver hook. It could also be used to revert state if the hook call aborts, which may be useful to some implementations?

@abright abright mentioned this pull request Sep 13, 2022
@abright abright force-pushed the token/transfer-integration-test branch 2 times, most recently from 9c41e14 to 39fc914 Compare September 14, 2022 06:07
Base automatically changed from token/transfer-integration-test to main September 14, 2022 07:06
@abright abright force-pushed the token/more-integration-tests branch 2 times, most recently from d0edd2f to 6ec08a3 Compare September 20, 2022 05:50
Doing this can unintentionally revert to the pre-hook state if the receiver hook carried out further operations on the balance received
…erywhere

can only link against one actor for shared structs, because of the invoke methods using un-mangled names (causing collisions at link time if more than one is included)
original recipient just keeps the tokens in this case, instead of rejecting the transfer they originally received
the caller can check for success or failure of the transfer and also has the TransferReturn data available for use
amount: token_params.amount,
};
let receipt = sdk::send::send(&Address::new_id(sdk::message::caller()), method_hash!("Burn"), RawBytes::serialize(&burn_params).unwrap(), TokenAmount::zero()).unwrap();
if !receipt.exit_code.is_success() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if this should explode or just return the result upstream (so failure to burn will keep the tokens) like Transfer does above

Copy link
Member

Choose a reason for hiding this comment

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

I would say it should abort and so should transfer, but I see you're using this return value in tests, so ok to keep this behaviour if it's useful.

amount: balance,
};
let receipt = sdk::send::send(&params.token_address, method_hash!("Burn"), RawBytes::serialize(&burn_params).unwrap(), TokenAmount::zero()).unwrap();
if !receipt.exit_code.is_success() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as the Receive hook version, explode here or fail quietly and let the caller figure it out?

Copy link
Member

Choose a reason for hiding this comment

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

I reckon fail for both

@abright abright changed the title WIP more integration tests More integration tests Sep 20, 2022
@abright abright marked this pull request as ready for review September 20, 2022 07:29
@coveralls
Copy link

coveralls commented Sep 20, 2022

Coverage Status

Coverage decreased (-0.3%) to 86.152% when pulling 61355fd on token/more-integration-tests into e9371ad on main.

frc46_token/src/token/mod.rs Outdated Show resolved Hide resolved
to_balance: self.balance_of(to)?,
recipient_data: ret.recipient_data,
}
};
Copy link
Member

Choose a reason for hiding this comment

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

Seeing this, I'm now wondering about yet another change to the hook return to encourage safety. A token impl should generally re-inspect state to create the return value. There's a possible optimisation where the CID hasn't changed, but the general case is to reload.

So now I think we should not return the ret from the hook, but force the caller to recreate it. But make that easy by adding token methods that create a TransferReturn, MintReturn etc from the current state. The hook return would only contain the recipient data.

So, sketching...

let hook_ret = hook.call()?;
self.reload()?; // reloads state only if CID has changed
return self.util.transfer_return(hook_ret);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think things get simpler with an approach like that. Might want to rename the underlying functions from mint to mint_init or something to make clearer that it's a multi-step process? I'll leave that alone for the moment but it's a quick enough change afterwards if we want it in there.

The reload in your example probably needs to take the original CID we got when flushing the state before the hook call, but this otherwise looks like a simple enough change. Will try it out once I've addressed other review feedback as it's a more complex change than those.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Had a go at it, definitely feels nicer to use when implementing a token.

Transfer(Address, RawBytes),
/// Burn incoming tokens
Burn,
}
Copy link
Member

Choose a reason for hiding this comment

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

Nice

testing/fil_token_integration/actors/test_actor/src/lib.rs Outdated Show resolved Hide resolved
amount: balance,
};
let receipt = sdk::send::send(&params.token_address, method_hash!("Burn"), RawBytes::serialize(&burn_params).unwrap(), TokenAmount::zero()).unwrap();
if !receipt.exit_code.is_success() {
Copy link
Member

Choose a reason for hiding this comment

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

I reckon fail for both

assert!(ret_val.msg_receipt.exit_code.is_success());
// check the receipt we got in return data
let receipt = ret_val.msg_receipt.return_data.deserialize::<Receipt>().unwrap();
assert!(!receipt.exit_code.is_success());
Copy link
Member

Choose a reason for hiding this comment

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

Critical to assert all the balances here, and after every transfer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think I've covered all these now.

Also added some extra checks of TransferReturn data in a couple of the more complex cases. I want to do more of that in a follow-up along with the remaining few test cases. The multi-layered stuff gets messy real quick because each struct serializes the next layer in its return_data or recipient_data. I want to spend some time coming up with a nicer way to handle that stuff but I figure it's a low priority for the moment.

@abright abright requested a review from anorth September 21, 2022 23:00
/// Replace the current state with another
/// The previous state is returned and can be safely dropped
pub fn replace(&mut self, state: TokenState) -> TokenState {
std::mem::replace(self.state, state)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is what I want. I think I was confused when I asked for it.

What I need would be be parallel with wrap, taking a mutable reference. I have a new State (embedded in another struct) that I've loaded into memory, and now want a token wrapping it. I want future mutations to mutate that reference in place (so I can persist later). I don't want to further mutate the state behind the old reference.

Perhaps I could just wrap again to get a new token in this case, but a re-wrap(&mut new_state) would still be convenient.

We should probably restore the old replace method for its use in testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What you're describing might be better done with std::mem::swap. Though it may also be better to just treat the Token as disposable and create a new one as needed. Which I guess is what we'd end up doing in a re-wrap function - create a new Token by taking everything but the state from the old one.

This new replace doesn't hurt the tests, it's a better version of what they were using before.

let supply = state.change_supply_by(amount)?;
Ok(MintReturn { balance, supply: supply.clone(), recipient_data: RawBytes::default() })
state.change_balance_by(&bs, owner_id, amount)?;
state.change_supply_by(amount)?;
Copy link
Member

Choose a reason for hiding this comment

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

Discussion: in most cases, when there is no recursion, the value returned here will be the right one. It would be nice to avoid traversing the balance table again to look up the value when that happens. So storing it in the intermediate for that case could help. That then raises the question of flowing information through to the token so that it knows whether it needs to look up a new balance. Maybe an internal dirty flag or mutation count?

I think this is a nuanced enough issue that you should write an issue and defer it to a new PR. Let's get it correct now, and then optimise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made issue #110 for it

#[derive(Debug)]
pub struct MintIntermediate {
/// Recipient address to use for querying balance
pub recipient: Address,
Copy link
Member

Choose a reason for hiding this comment

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

Should we restrict visibility of these to the crate? I'm undecided, so ok to leave public for now.

@abright abright merged commit a58f063 into main Sep 23, 2022
@abright abright deleted the token/more-integration-tests branch September 23, 2022 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants