Repository navigation
Support fee-exempt Bitget routes on Solana with co-signing - #5117
AryanGodara wants to merge 2 commits into
Conversation
Signed-off-by: Aryan Godara <aryangodara03@gmail.com>
…onse error Signed-off-by: Aryan Godara <aryangodara03@gmail.com>
|
Claude finished @AryanGodara's task in 3m 41s —— View job PR Review: Fee-exempt Bitget routes with co-signingReviewed the full diff against I left two inline comments:
Nothing blocking. The failure mode where Bitget's cosign becomes a hard dependency of every exempt settlement is already called out in the PR description, so I didn't re-flag it. |
| // Other signers sign the final message, so only now. A failure leaves | ||
| // the solution cached for a retry. | ||
| let transaction = if transaction.message.header().num_required_signatures > 1 { | ||
| let remaining = deadline.saturating_duration_since(Instant::now()); | ||
| self.solver.cosign(&transaction, remaining).await? | ||
| } else { | ||
| transaction | ||
| }; |
There was a problem hiding this comment.
The elapsed-deadline guard that protects the send (the confirm_timeout.is_zero() check just below at line 518-521, with the comment explaining why a zero timeout is handled before touching the network) is missing here before the cosign.
If the deadline has already passed by the time we reach this point (e.g. simulation was slow), remaining is zero and we still issue the /cosign request to Bitget. reqwest does not skip a zero-duration timeout — it either fires the request and times out immediately or, worse, runs the full round trip — so Bitget may sign a transaction that can never be submitted (the send guard below then returns DeadlineExceeded). That's a wasted, rate-limited call to Bitget's co-sign service.
Consider mirroring the guard before cosigning:
let transaction = if transaction.message.header().num_required_signatures > 1 {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::DeadlineExceeded);
}
self.solver.cosign(&transaction, remaining).await?
} else {
transaction
};| impl Solver<Dex> { | ||
| pub async fn cosign( | ||
| &self, | ||
| transaction: &VersionedTransaction, | ||
| ) -> Result<VersionedTransaction, dex::Error> { | ||
| self.quoter.cosign(transaction).await | ||
| } | ||
| } |
There was a problem hiding this comment.
Minor: cosign calls self.quoter.cosign(..) directly, bypassing the rate_limiter (and the concurrency bound) that solve goes through. Settles are far less frequent than solves, so this is likely fine in practice — but note that when a Bitget cosign starts failing and the driver retries each settle, these retries hit Bitget's /signBgwSolTx unthrottled. Worth a comment at least, or routing cosign through the same limiter if Bitget rate-limits that endpoint.
squadgazzz
left a comment
There was a problem hiding this comment.
I will do another round a bit later, since the changes touch pretty important mechanisms outside of the bitget solver logic.
| &self, | ||
| transaction: &VersionedTransaction, | ||
| ) -> Result<VersionedTransaction, Error> { | ||
| self.post::<dto::CosignResponse>(COSIGN_PATH, &dto::CosignRequest::new(transaction)) |
There was a problem hiding this comment.
This goes through the same egress-proxy location as /swap. In nodes/egress-proxy/nginx.conf.template, /bitget/solana/solve has limit_req zone=bitget_solana_solve burst=${BITGET_SOLANA_SOLVE_BURST} nodelay, staging sets 2 r/s with burst 1, and the overflow is a 429. The solve loop runs concurrent-requests = 2 against that same zone, so a cosign that lands next to two quotes gets the 429, maps to RateLimited, and the settle fails as SolverFailed. Neither the engine nor the driver retries it. This needs to be fixed.
| /// Sign the message's fee payer slot, which must be the signer's key. The | ||
| /// slots of any other required signers stay empty for them to fill. |
There was a problem hiding this comment.
With the sole-signer check gone, the driver accepts a multi-signer message from any engine, and the solve-time bundle simulation skips signature checks. So a Jupiter solution with a stray isSigner wins the auction and then fails every settle with a 501 from /cosign. Not new, before this, it died in encode, but it's still only caught after the auction. Would a solve-time check make sense, drop a solution needing a second signature unless the engine serves /cosign?
|
|
||
| // Other signers sign the final message, so only now. A failure leaves | ||
| // the solution cached for a retry. | ||
| let transaction = if transaction.message.header().num_required_signatures > 1 { |
There was a problem hiding this comment.
IIUC, this is the only place that decides a solution needs a cosigner, and it runs after the auction is won. At solve time, simulate_solution already compiles the message (resolved.unsigned()), so the header is known there, and the bundle simulation skips signature checks, so a solution with a second signer passes it. For an engine without /cosign that is a solution that wins and then fails every settle. Probably worth a cosign = true flag on the [[solvers]] entry? Without it, drop a multi-signer solution in simulate_solution as a proven failure. Then Jupiter can never reach this branch, and the flag is a driver-side kill switch next to the engine's fee setting.
Description
Lets the Bitget engine skip Bitget's 1 bp fee. Bitget only waives the fee on routes it cosigns, so these settlements need a second signature from Bitget. The driver still builds, simulates and signs the settlement. It only collects Bitget's signature before sending.
Changes
fee = "standard"(default, 1 bp) orfee = "exempt"(no fee)POST /cosign. It takes the driver-signed transaction and returns it with Bitget's signature, via Bitget's/signBgwSolTx./cosignafter simulating and checks the result before sending: same message, its own signature unchanged, every signature valid. A failure keeps the solution for a retry. There's no driver config for this.Signer::signnow signs the fee payer slot and leaves other signers' slots empty.RecordingSendertocow-solana-rpcbecause the pinnedsolana-rpc-clientmock can't decode v0 transactions.With
fee = "exempt", Bitget's co-sign service becomes a dependency of every Bitget settlement. If it fails, Bitget still wins auctions and each settle fails; switching back tofee = "standard"fixes that.How to test