Skip to main content

thruput_testkit/
agent.rs

1//! Agents and the context the harness hands them.
2
3use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use thruput_json_rpc_types::responses::{ReceiptExecution, TransactionReceipt};
9use thruput_xdr::contract::ContractEntrypointID;
10use thruput_xdr::types::{ContractAddress, SHA256Hash};
11
12use crate::account::{
13    Account, DeployOptions, Deployed, SubmittedDeploy, verify_deployment_receipt,
14};
15use crate::artifact::{
16    ArtifactEntry, ArtifactKind, PendingRegistration, RegistrationOwner, Registry,
17};
18use xdrs::{ReadXDR, WriteXDR};
19
20use crate::caller::Caller;
21use crate::clock::Clock;
22use crate::error::{Result, TestkitError};
23use crate::mailbox::{Envelope, Mailbox, Message};
24use crate::network::{Network, SubmittedTransaction};
25use crate::rng::AgentRng;
26
27enum RegistrationResolution {
28    NeedsRegistration,
29    Registered,
30    Recovered(Box<SubmittedDeploy>),
31}
32
33/// Stable identity of an agent within a scenario run.
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct AgentId {
36    index: usize,
37    name: Arc<str>,
38}
39
40impl AgentId {
41    pub(crate) fn new(index: usize, name: Arc<str>) -> Self {
42        Self { index, name }
43    }
44
45    /// Position of the agent in the scenario's agent list.
46    pub fn index(&self) -> usize {
47        self.index
48    }
49
50    /// Agent name.
51    pub fn name(&self) -> &str {
52        &self.name
53    }
54}
55
56type AgentFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
57type AgentBody = Box<dyn FnOnce(AgentContext) -> AgentFuture + Send>;
58
59/// An agent: a name plus the async body the scheduler runs.
60pub struct Agent {
61    name: Arc<str>,
62    body: AgentBody,
63}
64
65impl Agent {
66    /// Create an agent from a `name` and an async `body`.
67    ///
68    /// The body receives an [`AgentContext`] and returns `Ok(())` on success;
69    /// a returned error or a panic fails the scenario.
70    pub fn new<F, Fut>(name: impl Into<String>, body: F) -> Self
71    where
72        F: FnOnce(AgentContext) -> Fut + Send + 'static,
73        Fut: Future<Output = Result<()>> + Send + 'static,
74    {
75        let name: Arc<str> = Arc::from(name.into());
76        Self {
77            name,
78            body: Box::new(move |ctx| Box::pin(body(ctx))),
79        }
80    }
81
82    /// Agent name.
83    pub fn name(&self) -> &str {
84        &self.name
85    }
86
87    pub(crate) fn name_arc(&self) -> Arc<str> {
88        Arc::clone(&self.name)
89    }
90
91    pub(crate) fn into_body(self) -> AgentBody {
92        self.body
93    }
94}
95
96impl fmt::Debug for Agent {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_struct("Agent")
99            .field("name", &self.name)
100            .finish_non_exhaustive()
101    }
102}
103
104/// Everything an agent needs to interact with the world.
105///
106/// The context is the only channel through which an agent touches the network,
107/// the clock, randomness, and other agents. Agents must not reach around it to
108/// raw I/O or `std::time`.
109///
110/// Chain operations take `&self`: they go through the agent's [`Caller`],
111/// which can also be cloned out to hold independently of the context. Only
112/// receiving messages and drawing randomness take `&mut self`, since those
113/// consume per-agent streams in order, along with artifact deployment and
114/// registration. Those treat an in-flight registration from the same agent as
115/// a retry of the same request, so overlapping requests from one agent must
116/// not be possible.
117#[derive(Debug)]
118pub struct AgentContext {
119    id: AgentId,
120    caller: Caller,
121    clock: Clock,
122    rng: AgentRng,
123    mailbox: Mailbox,
124    artifacts: Arc<Registry>,
125}
126
127impl AgentContext {
128    pub(crate) fn new(
129        id: AgentId,
130        network: Network,
131        clock: Clock,
132        rng: AgentRng,
133        mailbox: Mailbox,
134        account: Account,
135        artifacts: Arc<Registry>,
136    ) -> Self {
137        Self {
138            id,
139            caller: Caller::new(account, network),
140            clock,
141            rng,
142            mailbox,
143            artifacts,
144        }
145    }
146
147    /// The on-chain wallet address of this agent's assigned account.
148    pub fn address(&self) -> ContractAddress {
149        self.caller.account.address()
150    }
151
152    /// Index of this agent's assigned account.
153    pub fn account_index(&self) -> u32 {
154        self.caller.account.index()
155    }
156
157    /// Deploy an instance of a registered artifact, registering its binary once
158    /// (on the first deploy of that artifact, in the same transaction) and
159    /// deploying by hash thereafter. Awaits the receipt; fails if execution is
160    /// not successful.
161    ///
162    /// Requests from one agent cannot overlap, since a second request would
163    /// be taken as a retry of the first:
164    ///
165    /// ```compile_fail,E0499
166    /// # async fn overlap(ctx: &mut thruput_testkit::AgentContext) {
167    /// let options = thruput_testkit::DeployOptions::funded(0);
168    /// let first = ctx.deploy_artifact("counter", options);
169    /// let second = ctx.deploy_artifact("counter", options);
170    /// let _ = (first.await, second.await);
171    /// # }
172    /// ```
173    pub async fn deploy_artifact(
174        &mut self,
175        name: &str,
176        options: DeployOptions<'_>,
177    ) -> Result<Deployed> {
178        loop {
179            match self.submit_deploy_artifact(name, options).await {
180                Ok(submitted) => return submitted.await_receipt().await,
181                Err(TestkitError::ArtifactRegistrationPending { .. }) => {
182                    let entry = self.artifacts.get(name)?;
183                    if let Some(deployed) = await_pending_registration(
184                        &self.caller.network,
185                        &entry,
186                        self.caller.account.registration_owner(),
187                    )
188                    .await?
189                    {
190                        return Ok(deployed);
191                    }
192                }
193                Err(error) => return Err(error),
194            }
195        }
196    }
197
198    /// Submit an artifact deployment without waiting for its receipt.
199    ///
200    /// Returns [`TestkitError::ArtifactRegistrationPending`] when another agent
201    /// is registering the same artifact. The error carries that transaction's
202    /// hash so a manual scenario can produce its block and await its receipt
203    /// before retrying this deployment.
204    /// The error also retains signed bytes for explicit caller recovery. Scheduler
205    /// statuses never replay or discard a retained attempt.
206    pub async fn submit_deploy_artifact(
207        &mut self,
208        name: &str,
209        options: DeployOptions<'_>,
210    ) -> Result<SubmittedDeploy> {
211        let entry = self.artifacts.get(name)?;
212        let mut pending = entry.registration_guard().await;
213        match reconcile_pending_registration(
214            &self.caller.network,
215            name,
216            &entry,
217            &mut pending,
218            self.caller.account.registration_owner(),
219            options,
220        )
221        .await?
222        {
223            RegistrationResolution::Registered => {
224                drop(pending);
225                return self
226                    .caller
227                    .account
228                    .submit_deploy_entry(&self.caller.network, &entry, false, options)
229                    .await;
230            }
231            RegistrationResolution::Recovered(submitted) => return Ok(*submitted),
232            RegistrationResolution::NeedsRegistration => {}
233        }
234        let prepared = self.caller.account.prepare_deploy_entry(
235            &self.caller.network,
236            &entry,
237            true,
238            options,
239        )?;
240        *pending = Some(prepared.pending_registration());
241        drop(pending);
242        let submitted = prepared.submit().await?;
243        Ok(submitted.with_registration(entry))
244    }
245
246    /// Explicitly register an artifact's binary, deploying its first instance in
247    /// the same transaction. Prefer [`AgentContext::deploy_artifact`] unless a
248    /// test needs to control when registration happens.
249    pub async fn register_artifact(
250        &mut self,
251        name: &str,
252        options: DeployOptions<'_>,
253    ) -> Result<Deployed> {
254        loop {
255            match self.submit_register_artifact(name, options).await {
256                Ok(submitted) => return submitted.await_receipt().await,
257                Err(TestkitError::ArtifactRegistrationPending { .. }) => {
258                    let entry = self.artifacts.get(name)?;
259                    if let Some(deployed) = await_pending_registration(
260                        &self.caller.network,
261                        &entry,
262                        self.caller.account.registration_owner(),
263                    )
264                    .await?
265                    {
266                        return Ok(deployed);
267                    }
268                }
269                Err(error) => return Err(error),
270            }
271        }
272    }
273
274    /// Submit an explicit artifact registration without waiting for its receipt.
275    ///
276    /// Returns [`TestkitError::ArtifactRegistrationPending`] when a registration
277    /// transaction is already in flight.
278    pub async fn submit_register_artifact(
279        &mut self,
280        name: &str,
281        options: DeployOptions<'_>,
282    ) -> Result<SubmittedDeploy> {
283        let entry = self.artifacts.get(name)?;
284        let mut pending = entry.registration_guard().await;
285        match reconcile_pending_registration(
286            &self.caller.network,
287            name,
288            &entry,
289            &mut pending,
290            self.caller.account.registration_owner(),
291            options,
292        )
293        .await?
294        {
295            RegistrationResolution::Registered => {
296                drop(pending);
297                return self
298                    .caller
299                    .account
300                    .submit_deploy_entry(&self.caller.network, &entry, true, options)
301                    .await;
302            }
303            RegistrationResolution::Recovered(submitted) => return Ok(*submitted),
304            RegistrationResolution::NeedsRegistration => {}
305        }
306        let prepared = self.caller.account.prepare_deploy_entry(
307            &self.caller.network,
308            &entry,
309            true,
310            options,
311        )?;
312        *pending = Some(prepared.pending_registration());
313        drop(pending);
314        let submitted = prepared.submit().await?;
315        Ok(submitted.with_registration(entry))
316    }
317
318    /// Deploy an instance by hash only, without carrying the binary. The
319    /// artifact must already be registered and committed, or the transaction
320    /// fails. Prefer [`AgentContext::deploy_artifact`] unless a test is
321    /// asserting on the hash-only path.
322    pub async fn deploy_registered(
323        &self,
324        name: &str,
325        options: DeployOptions<'_>,
326    ) -> Result<Deployed> {
327        self.submit_deploy_registered(name, options)
328            .await?
329            .await_receipt()
330            .await
331    }
332
333    /// Submit a hash-only artifact deployment without waiting for its receipt.
334    pub async fn submit_deploy_registered(
335        &self,
336        name: &str,
337        options: DeployOptions<'_>,
338    ) -> Result<SubmittedDeploy> {
339        let entry = self.artifacts.get(name)?;
340        self.caller
341            .account
342            .submit_deploy_entry(&self.caller.network, &entry, false, options)
343            .await
344    }
345
346    /// The network identity (contract hash) of a registered artifact.
347    pub fn contract_hash(&self, name: &str) -> Result<SHA256Hash> {
348        Ok(self.artifacts.get(name)?.hash())
349    }
350
351    /// Whether the harness has observed this artifact registered this run.
352    pub fn is_registered(&self, name: &str) -> Result<bool> {
353        Ok(self.artifacts.get(name)?.registered())
354    }
355
356    /// Deploy raw contract bytes not held in the registry, awaiting the receipt.
357    /// The binary is carried in every such transaction. Fails if execution is
358    /// not successful.
359    pub async fn deploy(
360        &self,
361        artifact: &[u8],
362        kind: ArtifactKind,
363        options: DeployOptions<'_>,
364    ) -> Result<Deployed> {
365        self.submit_deploy(artifact, kind, options)
366            .await?
367            .await_receipt()
368            .await
369    }
370
371    /// Submit a raw artifact deployment without waiting for its receipt.
372    pub async fn submit_deploy(
373        &self,
374        artifact: &[u8],
375        _kind: ArtifactKind,
376        options: DeployOptions<'_>,
377    ) -> Result<SubmittedDeploy> {
378        self.caller
379            .account
380            .submit_deploy(&self.caller.network, artifact, options)
381            .await
382    }
383
384    /// Submit an exact raw native token transfer from the runtime's bootstrap faucet.
385    /// Existing-network fixtures start empty; local fixtures receive explicit genesis allocations.
386    /// Retry uncertain local funding with the same amount on this scenario/network handle.
387    pub async fn submit_fund(&self, amount: u128) -> Result<SubmittedTransaction> {
388        self.caller
389            .network
390            .submit_fund(self.address(), amount)
391            .await
392    }
393
394    /// Fund this account explicitly and require a successful matching receipt.
395    /// In manual mode, use `submit_fund`, produce a block, then await the receipt.
396    pub async fn fund(&self, amount: u128) -> Result<TransactionReceipt> {
397        let receipt = self.submit_fund(amount).await?.await_receipt().await?;
398        if !matches!(receipt.execution, ReceiptExecution::Return { .. }) {
399            return Err(TestkitError::Reverted {
400                method: "fund".to_owned(),
401                status: format!("{:?}", receipt.execution),
402            });
403        }
404        Ok(receipt)
405    }
406
407    /// Call a deployed contract method from this agent's account, awaiting the
408    /// receipt. The receipt is returned even when execution reverted.
409    pub async fn call(
410        &self,
411        target: ContractAddress,
412        entrypoint: ContractEntrypointID,
413        calldata: &[u8],
414    ) -> Result<TransactionReceipt> {
415        self.caller.call(target, entrypoint, calldata).await
416    }
417
418    /// Submit a contract call without waiting for its receipt.
419    pub async fn submit_call(
420        &self,
421        target: ContractAddress,
422        entrypoint: ContractEntrypointID,
423        calldata: &[u8],
424    ) -> Result<SubmittedTransaction> {
425        self.caller.submit_call(target, entrypoint, calldata).await
426    }
427
428    /// This agent's identity.
429    pub fn id(&self) -> &AgentId {
430        &self.id
431    }
432
433    /// This agent's name.
434    pub fn name(&self) -> &str {
435        self.id.name()
436    }
437
438    /// Handle to the network the scenario is running against.
439    pub fn network(&self) -> &Network {
440        &self.caller.network
441    }
442
443    /// This agent's account bound to the network, for submitting transactions
444    /// independently of the context. Clone it to keep it past the borrow.
445    pub fn caller(&self) -> &Caller {
446        &self.caller
447    }
448
449    /// The scenario's virtual clock.
450    pub fn clock(&self) -> &Clock {
451        &self.clock
452    }
453
454    /// This agent's deterministic random stream.
455    pub fn rng(&mut self) -> &mut AgentRng {
456        &mut self.rng
457    }
458
459    /// CXS-encode `msg` and send it to the agent named `to`.
460    pub fn send<T>(&self, to: &str, msg: &T) -> Result<()>
461    where
462        T: WriteXDR + ?Sized,
463    {
464        let bytes = xdrs::cxs::encode(msg).map_err(TestkitError::encode)?;
465        self.mailbox.send_to(to, bytes)
466    }
467
468    /// Wait for the next inbound message, decoding its CXS payload as `T`.
469    ///
470    /// Returns `None` once all peers finish; the inner `Result` is `Err` when
471    /// the payload does not decode as `T`.
472    pub async fn recv<T>(&mut self) -> Option<Result<Message<T>>>
473    where
474        T: for<'r> ReadXDR<'r>,
475    {
476        self.mailbox.recv().await.map(Envelope::decode)
477    }
478
479    /// Take a pending inbound message without waiting, decoding its payload
480    /// as `T`.
481    pub fn try_recv<T>(&mut self) -> Option<Result<Message<T>>>
482    where
483        T: for<'r> ReadXDR<'r>,
484    {
485        self.mailbox.try_recv().map(Envelope::decode)
486    }
487
488    /// Wait for the next message from the agent named `from`, decoding its CXS
489    /// payload as `T`.
490    ///
491    /// Messages from other senders that arrive first are held for later
492    /// [`recv`](Self::recv) / `recv_from` calls rather than dropped. Returns
493    /// `None` once all peers finish without a matching message; the inner
494    /// `Result` is `Err` when the payload does not decode as `T`.
495    pub async fn recv_from<T>(&mut self, from: &str) -> Option<Result<Message<T>>>
496    where
497        T: for<'r> ReadXDR<'r>,
498    {
499        self.mailbox.recv_from(from).await.map(Envelope::decode)
500    }
501
502    /// Take a pending message from the agent named `from` without waiting,
503    /// decoding its payload as `T`.
504    pub fn try_recv_from<T>(&mut self, from: &str) -> Option<Result<Message<T>>>
505    where
506        T: for<'r> ReadXDR<'r>,
507    {
508        self.mailbox.try_recv_from(from).map(Envelope::decode)
509    }
510
511    /// Wait for the next message from any of the agents named in `from`,
512    /// decoding its CXS payload as `T`. [`Message::from`] identifies which one.
513    ///
514    /// Like [`recv_from`](Self::recv_from), messages from senders outside the
515    /// set are held for later calls rather than dropped.
516    pub async fn recv_from_any<T>(&mut self, from: &[&str]) -> Option<Result<Message<T>>>
517    where
518        T: for<'r> ReadXDR<'r>,
519    {
520        self.mailbox.recv_from_any(from).await.map(Envelope::decode)
521    }
522
523    /// Take a pending message from any of the agents named in `from` without
524    /// waiting, decoding its payload as `T`.
525    pub fn try_recv_from_any<T>(&mut self, from: &[&str]) -> Option<Result<Message<T>>>
526    where
527        T: for<'r> ReadXDR<'r>,
528    {
529        self.mailbox.try_recv_from_any(from).map(Envelope::decode)
530    }
531}
532
533impl AsRef<Caller> for AgentContext {
534    fn as_ref(&self) -> &Caller {
535        &self.caller
536    }
537}
538
539async fn reconcile_pending_registration(
540    network: &Network,
541    name: &str,
542    entry: &Arc<ArtifactEntry>,
543    pending: &mut Option<PendingRegistration>,
544    caller: RegistrationOwner,
545    options: DeployOptions<'_>,
546) -> Result<RegistrationResolution> {
547    let Some(attempt) = pending.clone() else {
548        return if entry.registered() {
549            Ok(RegistrationResolution::Registered)
550        } else {
551            Ok(RegistrationResolution::NeedsRegistration)
552        };
553    };
554    if attempt.owner == caller && attempt.effects != options.effects()? {
555        return Err(TestkitError::DeploymentRequestMismatch {
556            name: name.to_owned(),
557        });
558    }
559    if entry.registered() && attempt.owner != caller {
560        return Ok(RegistrationResolution::Registered);
561    }
562    let tx_hash = attempt.tx_hash;
563    let Some(receipt) = network.receipt(tx_hash).await? else {
564        // Scheduler observations do not prove nonexecution or authorize another write.
565        return Err(TestkitError::ArtifactRegistrationPending {
566            name: name.to_owned(),
567            tx_hash,
568            signed_transaction: attempt.tx_hex.clone(),
569        });
570    };
571    verify_deployment_receipt(tx_hash, attempt.address, &attempt.tx_hex, &receipt)?;
572    if matches!(receipt.execution, ReceiptExecution::Return { .. }) {
573        entry.mark_registered();
574        if attempt.owner == caller {
575            *pending = None;
576            Ok(RegistrationResolution::Recovered(Box::new(
577                SubmittedDeploy::recovered(network.clone(), &attempt, receipt),
578            )))
579        } else {
580            Ok(RegistrationResolution::Registered)
581        }
582    } else {
583        *pending = None;
584        Err(TestkitError::Deploy {
585            detail: format!("{:?}", receipt.execution),
586        })
587    }
588}
589
590async fn await_pending_registration(
591    network: &Network,
592    entry: &ArtifactEntry,
593    caller: RegistrationOwner,
594) -> Result<Option<Deployed>> {
595    let attempt = {
596        let pending = entry.registration_guard().await;
597        pending.clone()
598    };
599    let Some(attempt) = attempt else {
600        return Ok(None);
601    };
602    let tx_hash = attempt.tx_hash;
603    let receipt = network.await_receipt(tx_hash).await?;
604    finish_registration_receipt(entry, &attempt, receipt, caller).await
605}
606
607async fn finish_registration_receipt(
608    entry: &ArtifactEntry,
609    attempt: &PendingRegistration,
610    receipt: TransactionReceipt,
611    caller: RegistrationOwner,
612) -> Result<Option<Deployed>> {
613    verify_deployment_receipt(attempt.tx_hash, attempt.address, &attempt.tx_hex, &receipt)?;
614    let succeeded = matches!(receipt.execution, ReceiptExecution::Return { .. });
615    if !succeeded || attempt.owner == caller {
616        entry.finish_registration(attempt.tx_hash, succeeded).await;
617    } else {
618        entry.mark_registered();
619    }
620    if !succeeded {
621        return Err(TestkitError::Deploy {
622            detail: format!("{:?}", receipt.execution),
623        });
624    }
625    Ok((attempt.owner == caller).then_some(Deployed {
626        address: attempt.address,
627        tx_hash: attempt.tx_hash,
628        receipt,
629        registered: true,
630    }))
631}