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

Rosetta: Handle single GasCoin transfers #20592

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

nonvis
Copy link
Contributor

@nonvis nonvis commented Dec 11, 2024

Description

Describe the changes or additions included in this PR.

Test plan

How did you test the new or updated feature?


Release notes

Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required.

For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates.

  • Protocol:
  • Nodes (Validators and Full nodes):
  • Indexer:
  • JSON-RPC:
  • GraphQL:
  • CLI:
  • Rust SDK:
  • REST API:

Copy link

vercel bot commented Dec 11, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
sui-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 3, 2025 6:08am
3 Skipped Deployments
Name Status Preview Comments Updated (UTC)
multisig-toolkit ⬜️ Ignored (Inspect) Visit Preview Jan 3, 2025 6:08am
sui-kiosk ⬜️ Ignored (Inspect) Visit Preview Jan 3, 2025 6:08am
sui-typescript-docs ⬜️ Ignored (Inspect) Visit Preview Jan 3, 2025 6:08am

@nonvis nonvis temporarily deployed to sui-typescript-aws-kms-test-env December 11, 2024 05:45 — with GitHub Actions Inactive
for command in commands {
match command {
SuiCommand::TransferObjects(objs, _) => {
if objs.len() == 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 we instead have this feature handle well if any of the transferred objects is SuiArgument::GasCoin, instead of only when the only object transferred is a gas-coin?

Copy link
Contributor

@nikos-terzo nikos-terzo Dec 11, 2024

Choose a reason for hiding this comment

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

This ofc would need changes below as well. What I am thinking of is checking whether a Gas transfer exists. If it does and it can be matched with balance-changes between gas-coin-owner and recipient, then we replace the two with the appropriate PaySui and correct Gas operations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handles if any of the transferred objects is GasCoin

Some(amount) => amount.value,
None => 0,
};
if account.address == sender {
Copy link
Contributor

Choose a reason for hiding this comment

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

As @patrickkuo pointed out, we shouldn't use sender but previous gas-coin owner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if account.address == sender {
// sender's balance needs to be adjusted for gas
amount -= gas_used;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should not edit every balance-change. Only the gas-coin sender and the recipient ones.

Copy link
Contributor

@nikos-terzo nikos-terzo Dec 11, 2024

Choose a reason for hiding this comment

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

And we should also make sure that these addition and subtraction each happen only once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 568 to 592
let mut is_single_gascoin_transfer = false;
match tx {
SuiTransactionBlockKind::ProgrammableTransaction(pt) => {
let SuiProgrammableTransactionBlock {
inputs: _,
commands,
} = &pt;
for command in commands {
match command {
SuiCommand::TransferObjects(objs, _) => {
if objs.len() == 1 {
match objs[0] {
SuiArgument::GasCoin => {
is_single_gascoin_transfer = true;
}
_ => {}
}
}
}
_ => {}
}
}
}
_ => {}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

lets extract this to a function to make it more readable.

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've extracted it to a function.

inputs: _,
commands,
} = &pt;
for command in commands {
Copy link
Contributor

Choose a reason for hiding this comment

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

In case we find a command with gas-coin we can stop iterating.
Maybe something like commands.into_iter().find(|c| { ... }) would be more suitable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@nikos-terzo nikos-terzo left a comment

Choose a reason for hiding this comment

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

  1. Argument::GasCoin can be have any index in the transfer-args input. We need to catch these cases as well.
  2. We need to make sure that we do not double-count the gas used in case more than one SuiPay or SuiBalanceChange refer to the gas sender-recipient pair.

.into_iter()
.find(|command| match command {
SuiCommand::TransferObjects(objs, _) => {
objs.len() > 0 && objs[0] == SuiArgument::GasCoin
Copy link
Contributor

Choose a reason for hiding this comment

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

This would fail if the GasCoin is not the first object transferred.
Something like objs.find(|&obj| obj == SuiArgument::GasCoin).is_some() objs.into_iter().any(|&obj| obj == SuiArgument::GasCoin) would succeed in case any of the objects transferred is the GasCoin.
I guess as we should identify such transfers, we can also rename the function to is_gascoin_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.

Added a search for any place for GasCoin and renamed the function to is_gascoin_transfer

// who paid for the txn (this is the format Rosetta wants to process)
operations.push(Operation::gas(prev_gas_owner, gas_used))
}
OperationType::SuiBalanceChange => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if multiple balance changes exist there for sender or recipient? I think this block might double-count gas.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

balance_changes are derived from effects field and even though there were multiple transfers for sender/recipient, there is one entry per address so I think there is no risk of double-count here.

Copy link
Contributor

@nikos-terzo nikos-terzo Dec 18, 2024

Choose a reason for hiding this comment

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

Could there be the case though, that the sender also receives an amount equal to the gas-coin transferred, so that there is no balance-change for them? I know that this is far-fetched, but every SUI tx passes through this parsing. In that case this operation mapping would fail, correct? Lastly, the PaySui operations need to have their amounts match. See also above comment about explicit handling versus mapping.

Edit: Also even if the amount is not equal, it would introduce a discrepancy between the PaySui for receiver and the PaySui for sender operations.
Example ptb:

  1. SUI is unlocked from somewhere (eg. a Liquidity Pool),
  2. merged with Gas-coin
  3. gas is sent to the recipient.

tx.data.transaction().clone(),
tx.data.gas_data().owner,
gas_owner,
gas_used,
);

Copy link
Contributor

Choose a reason for hiding this comment

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

Every transaction that happens on-chain will pass from here. We need to be extra careful that we do not mess up the balance-changes. Does it make sense to add a sanity check that operations balance changes match the tx balance changes after editing the operations with gas-coins transfer?
@patrickkuo , @nonvis wdyt?

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 don't think a sanity check is required. First, the balance-changes is edited only when GasCoin is referenced for transferObjects, which occurs infrequently. And if they do occur, only two types of operations are used: BalanceChange and Gas. process_gascoin_transfer function processes these two types of operations.
So in terms of the number of operations stored before/after process_gascoin_transfer, they would be the same.

}
});
assert_eq!(balance, 0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Optimally, a more complex transaction involving gas-coin transfer should be tested too. Something involving a balance-change from eg. a DEX tx along with a gas-coin transfer. I know this is an extreme edge-case, but if not every, almost every tx on-chain will actually be passed through this endpoint.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can copy an existing DEX tx from chain and try manually adding a gas-coin-transfer to it?

}
});
}
_ => {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Theoretically this should be unreachable, correct? If so, can we return an Error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Returns an Error

operation.account.map(|account| {
let mut amount = match operation.amount {
Some(amount) => amount,
None => return,
Copy link
Contributor

Choose a reason for hiding this comment

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

Here we ignore an Operation. The operation will be removed from the operations list. Is this case unreachable? If so, can we return an error with the tx-digest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I made it return an Error so that it won't be secretly removed from the operations list.

_ => {}
}
});
// sanity check to make sure all the operations have been processed
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any way that this iterator is not consumed. By sanity check I meant checking that tx-balance-changes much the ones the operations refer to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the sanity check

) -> Result<Vec<Operation>, anyhow::Error> {
let mut gascoin_transfer_operations = vec![];
if Self::is_gascoin_transfer(tx) {
coin_change_operations.into_iter().for_each(|operation| {
Copy link
Contributor

@nikos-terzo nikos-terzo Dec 18, 2024

Choose a reason for hiding this comment

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

I am wondering whether it would be cleaner, to replace this mapping of balance-changes to a more explicit approach:

  1. Check whether is_gascoin_transfer.
  2. Check whether SUI balance-changes (SuiBalanceChange with SUI) exist between sender and recipient of gas-object, with a matching amount bigger than gas-used.
  3. If both of the above are true, edit these 3 operations:
    • recipient Gas
    • sender balance-change of SUI -> PaySui
    • recipient balance-change of SUI -> PaySui,
      otherwise if 1 is true but 2 is not*, create the missing PaySui operation. Maybe create two new SuiBalanceChange operations (other balance-changes might exist for sender/recipient, for different reasons).

*A little far-fetched, but the sender could have received the exact amount of the gas-coin (no-balance-change), or the sender could have received a larger balance than the gas-coin, making it a PaySui which increases the sender's balance. This shouldn't happen.
Also due to the nature of ptbs, there might be the case that even the recipient could receive more balance than the one which was transferred by the gas-coin, for other reasons.

Copy link
Contributor

@nikos-terzo nikos-terzo Dec 18, 2024

Choose a reason for hiding this comment

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

@patrickkuo , @nonvis wdyt? I believe explicitly editing the operations in question, instead of mapping all the operations is a better approach.

// who paid for the txn (this is the format Rosetta wants to process)
operations.push(Operation::gas(prev_gas_owner, gas_used))
}
OperationType::SuiBalanceChange => {
Copy link
Contributor

@nikos-terzo nikos-terzo Dec 18, 2024

Choose a reason for hiding this comment

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

Could there be the case though, that the sender also receives an amount equal to the gas-coin transferred, so that there is no balance-change for them? I know that this is far-fetched, but every SUI tx passes through this parsing. In that case this operation mapping would fail, correct? Lastly, the PaySui operations need to have their amounts match. See also above comment about explicit handling versus mapping.

Edit: Also even if the amount is not equal, it would introduce a discrepancy between the PaySui for receiver and the PaySui for sender operations.
Example ptb:

  1. SUI is unlocked from somewhere (eg. a Liquidity Pool),
  2. merged with Gas-coin
  3. gas is sent to the recipient.

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