-
Notifications
You must be signed in to change notification settings - Fork 1
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
Next #9
base: adutit-commit
Are you sure you want to change the base?
Next #9
Conversation
* refactor: update how program logs handle whitespace * chore: linter * chore: updaet gas service program id
Also remove the need of passing bump in constructor.
* feat: completes governance module docs * fix: clarify the large message payload feature on GMP messages * fix: use shareable links for diagrams * fix: clarify only one execution can happen
If this is not done, anyone can initialize the ITS root config and assign the operator role to anyone. Signed-off-by: Guilherme Felipe da Silva <[email protected]>
* docs(axelar-solana-its): add README * docs(axelar-solana-gas-service): add empty README This will be populated later. Signed-off-by: Guilherme Felipe da Silva <[email protected]> * docs(axelar-solana-its): add PDA owner information --------- Signed-off-by: Guilherme Felipe da Silva <[email protected]>
* feat: only deployer can init governance program * refactor: use fqdns
* chore: change gov program id * feat: add test helper for finding multiple logs * fix: flaky tests, we need to expect more than one error * chore: update to gov module to a new id * chore: use new test helper in more code
* Add LICENSE, CONTRIBUTING, CODE_OF_CONDUCT --------- Co-authored-by: Piotr Olszewski <[email protected]>
|
||
## Exceptions of the `accounts[]` rule: ITS & Governance | ||
|
||
[Interchain Token Service](https://github.com/axelarnetwork/interchain-token-service/blob/main/DESIGN.md#interchain-tokens) and [Governance contract](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/test/utils.js#L24-L44) have a legacy ABI interface that must be respected. This means that we cannot enforce arbitrary new encoding for the existing protocols; we only need them to be able to interact with Solana. As a result, the Relayer has special handling when interacting with ITS & Governance contracts; it will decode the `abi` encoded messages, introspect into the message contents and attempt to deterministically derive all the desired accounts for the action the message wants to make. This approach only works when the message layout is known beforehand (meaning that the Relayer can decode it) AND the Relayer has hardcoded custom logic to derive the accounts. This means that this special handling is not possible for the generic case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does interchain token service need special handling? Surely this is not the only existing app that will make calls to Solana.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does interchain token service need special handling?
- Because the abi spec for ITS needs to be consistent across all chains (any evm chain needs to be able to talk to our Solana ITS and vice-versa).
- This means that we cannot force arbitrary
accounts[]
data to be provided by the source chain. e.g TheINTERCHAIN_TOKEN
does not have a place to provide such info - Without knowing the
accounts[]
on the relayer, we cannot create a transaction to send (hence why we even have the extra necessity of an encoding layer)
So, how do we solve this for ITS to get the accounts, to construct a tx?
We, on the relayer, introspect into the ITS payload and query the node state. We have developed ITS in a way that allows us to deterministically compute the accounts based on the message contents. This is ITS-specific handling that allows us to not break the existing interface. Meaning that all EVM ITSs will be able to communicate with the Solana ITS.
- this is where we have the special handling on the relayer
- this is the code that is responsible for introspecting into the message payload and deriving the accounts
Surely this is not the only existing app that will make calls to Solana
We can handle the generic case as long as the source chain also provides the accounts[]
in the message payload like our relayer can parse it. Our Memo program is a good example:
Otherwise, it would require adding a patch to the relayer to add another special-case handling -- assuming the special-case handling can even be done, as it requires designing the Solana program with that in mind up-front. In many cases, the accounts cannot be computed deterministically as we did for ITS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also how does the relayer know to perform this logic versus the other? Are the ITS contract addresses whitelisted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, relayer has special handling based on the destination address specified in the GMP message:
match destination_address {
axelar_solana_its::ID => {
let ix = its_instruction_builder::build_its_gmp_instruction(
signer,
gateway_incoming_message_pda,
gateway_message_payload_pda,
message.clone(),
payload,
solana_rpc_client,
)
.await?;
send_transaction(solana_rpc_client, keypair, ix).await?
}
_ => {
let ix = axelar_executable::construct_axelar_executable_ix(
message,
&payload,
gateway_incoming_message_pda,
gateway_message_payload_pda,
)?;
send_transaction(solana_rpc_client, keypair, ix).await?
}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After thinking more, I think this needs to be a standard part of the protocol that apps can opt out of passing the accounts. Maybe in that case they use their own relayer, or we have some system that can figure out the accounts in the general case. But our design should be flexible, this sort of seems like right now the only usable apps are the whitelisted apps.
|
||
This means that the base interface for GMP messages defined by the Axelar protocol is not expressive enough for communicating with Solana programs - because there’s no place to put the account arrays. Therefore a workaround is needed, where we need to provide the information about the accounts within the scope of the existing `ContractCall` data structure. | ||
|
||
Solanas `axelar-executable` now expects that the `payload` emitted on the source chain also includes all the `account[]` data for the Relayer to create a proper transaction. If the `account[]` is absent on the GMP call, the Relayer cannot know what accounts to provide when calling the destination contract. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how exactly is the caller supposed to get this information in practice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
traditionally the frontend for the user computes this. Every client interacting with a Solana contract needs to be aware of the intricacies of the programs memory layout, pre-compute the accounts and provide them up-front when creating the transaction.
Example from the docs on how to create a transaction in JS
The same applies to the querying state. You don't query the state in Solana by interacting with the contract. You fetch the raw bytes at a given account address from the chain, and then the app must know how to deserialize the contents. And it's the app's responsibility to figure out the account it even needs to fetch.
In practice all of the computations are completely app dependant and completely arbitrary. Same as in Solidy you may compute memory slots up-front (link to AxelarAmplifierGateway), you must do the same on Solana for every piece of storage, and the end-user must provide it in the tx args.
Example from the Solana spl-token npm package -- they have client side logic to compute the accounts that must be provided up-front when making the transaction. This logic must be equivalent to the one we have inside the contracts, as it's fundamental to the on-chain security checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also can you remind me how wormhole handles this situation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wormhole.
The source chain is fully responsible for producing a payload that can be interpreted by the relayer (eg hosted by the dapp, or the frontend that performs manual relaying), including all of the accounts.
Because Wormhole can run a custom relayer for each app, it also means that the encoding of the message is entirely up to the relayer / app as well (figuring out how to retrieve the necessary accounts for the interaction).
For their token bridging, the "manual relaying", where the user is responsible for sending all txs on both chains, they have the following implementation:
- for wrapped tokens they derive the accounts "on the relayer" (aka users frontend) similarly as we do right now on the relayer for ITS. As you can see, it's custom logic that is fully relayer-specific and not a generic solution.
Technically, it's up to the relayer to figure out how to interpret the binary data published on the source chain -- it can very well be a bincoded
Solana tx that only requires signing & publishing (a bincoded
solana tx already includes all the accounts inside of it + the payload).
- But a downside of pre-encoding via
bincode
on the source chain is that you still end up encoding all the data and the accounts (no benefit there), and you are forced to use bincode, which Solidity does not support, meaning you cannot produce a payload fully like that on-chain. Pro: relayer only signs arbitrary data & sends the tx away; simpler code.
|
||
| action | tx count | description | | ||
| - | - | - | | ||
| Relayer calls [`Approve Message` (link to the processor)](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/approve_message.rs). | For each GMP message in the `ExecuteData` | <ol><li>Validating that a `message` is part of a `payload digest` using Merkle Proof.</li><li>Validating that the `payload digest` corresponds to `Signature Verification PDA`, and it has reached its quorum.</li><li>Validating that the `message` has not already been initialized</li><li>Initializes a new PDA (called `Incoming Message PDA`) responsible for tracking a message's `approved`/`executed` state. The core seed of this PDA is `command_id`. You can read more about `command_id` in the [EVM docs #replay prevention section](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/main/contracts/gateway/INTEGRATION.md#replay-prevention); our implementation is the same.</li><li>This action emits a log for the Relayer to capture.</li><li>Repeat this tx for every `message` in a batch.</li></ol> | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't command_id be message_id? command_id is legacy, most everywhere in amplifier uses message_id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During a refactoring ~8 months ago, the term command_id
changed its meaning; you may be still referring to the old one?
While it's true that the amplifier (ampd and cosmwasm contracts) uses message_id
, the reference implementation uses the term command_id
for referring to a hash of source_chain
+ message_id
.
Amplifier guarantees that the combination of source chain and message id will always be unique and fixed for a unique message. Hence, a deterministic command id can be derived based on the source chain and message id, and mark as executed to prevent replay.
The EVM gateway derives the command id as follows. External gateways for other chains can choose to use their own encoding scheme if preferred, as long as it's deterministic and includes the same values as below.
// sourceChain is guaranteed to not contain `_` character
bytes32 commandId = keccak256(bytes(string.concat(sourceChain, '_', messageId)));
| Relayer calls `Initialize Payload Verification Session` on the Gateway [[link to the processor]](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/initialize_payload_verification_session.rs) | 1 | This creates a new PDA that will keep track of the verified signatures. The `payload digest` is used as the core seed parameter for the PDA. This is safe because a `payload digest` will only be duplicated if the `verifier set` remains the same (this is often the case) AND all of the messages are the same. Even if all the messages remain the same, `Axelar Solana Gateway` has idempotency on a per-message level, meaning duplicate execution is impossible. | | ||
| The Relayer sends a tx [`VerifySignature` (link to the processor)](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/verify_signature.rs). | For each `verifier` + Signature in the `ExecuteData` that signed the payload digest | The core logic is that we: <ol><li>ensure that the `verifier` is part of the `verifier set` that signed the data using Merkle Proof. </li><li>check if the `signature` is valid for a given `payload digest` and if it matches the given `verifier` (by performing ECDSA recovery).</li><li>update the `signature verification PDA` to track the current weight of the verifier that was verified and the index of its Signature</li><li>repeat this tx for every `signature` until the `quorum` has been reached</li></ol> | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can a malicious relayer mess up this process? i.e. submitting bad signatures, bad proofs, some other operation, while the honest relayer is trying to submit all of the correct data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the operations made by the malicious relayer will fail:
- a bad signature will revert the transaction by the malicious relayer
- a bad proof will not verify against none of the Merkle roots
as a result, the on-chain tracker PDA state will not be affected, and the honest relayer can continue sending the proper data without disturbances.
|
||
| Gateway Instruction | Use Case | Caveats | | ||
| - | - | - | | ||
| [Call Contract](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/instructions.rs#L52-L67) | When you can create the data fully on-chain. Or When the data is small enough to fit into tx arguments | Even if you can generate all the data on-chain, the Solana tx log is limited to 10kb. And if your program logs more than that, there won't be any error on the transaction level. The log will be truncated, and the message will be malformed. **Please be careful when making this API call.** | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will the payload hash a least be logged in this situation (truncated log)?
- On Solana, there is no `msg.sender` concept as in Solidity. | ||
- On Solana `program_id`'s **cannot** be signers. | ||
- On Solana, only PDAs can sign on behalf of a program. The only way for programs to send messages is to create PDAs that use [`invoke_signed()`](https://docs.rs/solana-cpi/latest/solana_cpi/fn.invoke_signed.html) and sign over the CPI call. | ||
- The interface of `axelar_solana_gateway::GatewayInstruction::CallContract` instruction defines that the first account in the `accounts[]` must be the `program_id` that is sending the GMP payload. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this validated somehow? Like what if a contract puts a different account in the first slot, and effectively spoofs the sender. Is this possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh it looks like the next line talks about the validation, but it's unclear to me how this works exactly
No description provided.