Skip to content

Should the processor expose sync APIs? #2501

@huitseeker

Description

@huitseeker

Packages versions

miden-vm: 0.21.0 (upcoming)

Bug description

Processor API: Should it be async or sync?

We're hitting test failures in miden-base on miden-vm next (v0.21.0, unreleased) when using the P3 VM processor from async test functions. This raises a question about what the processor API should look like.

The test failure happens at processor/src/fast/mod.rs:923:

pub fn execute_for_trace_sync(...) -> Result<...> {
    let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
    rt.block_on(self.execute_for_trace(program, host, fragment_size))
}

When called from an async test function, it panics:

thread 'test_ecdsa_acl' panicked at processor/src/fast/mod.rs:925:12:
Cannot start a runtime from within a runtime.

Here's the call chain in miden-base:

#[tokio::test]
async fn test_ecdsa_acl() -> Result<()> {
    // ... setup ...
    prove_and_verify_transaction(tx)?;  // calls prover.prove()
                                        // which calls processor.execute_for_trace_sync()
                                        // which tries to create a new Tokio runtime
                                        // panic!
}

The processor has both async methods like execute_for_trace() and sync wrappers like execute_for_trace_sync(). The sync wrappers create their own Tokio runtime and call block_on(). This works from sync code but breaks from async contexts.

A few options:

Async only

Remove the *_sync() wrappers. Users who need sync can create their own runtime:

let rt = tokio::runtime::Runtime::new()?;
rt.block_on(processor.execute_for_trace(...))?;

Callers control async execution.

Detect existing runtime

Keep sync wrappers but check if we're already in a runtime:

  match tokio::runtime::Handle::try_current() {
      Ok(_handle) => {
          // We're in a runtime - use block_in_place to avoid blocking the worker thread
          tokio::task::block_in_place(|| {
              // Create a new runtime in this blocking context
              let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
              rt.block_on(self.execute(program, host))
          })
      },
      Err(_) => {
          // No runtime exists - create one and use it
          let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
          rt.block_on(self.execute(program, host))
      },
  }

Works in both contexts. Calling block_on() from within an existing runtime isn't ideal but technically works.

Spawn blocking

Use spawn_blocking() to move work to another thread when already in a runtime. More complex. Thread overhead.

What makes the most sense? The Winterfell VM was fully synchronous, so sync wrappers were probably added to match that API. Now that P3 uses async internally, maybe it's worth rethinking this.

The same pattern appears in execute_sync() and execute_sync_mut() in the same file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    plonky3Related to Plonky3 migrationprocessorRelated to Miden VM processor

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions