Skip to content

[Feature] Add dynamic dispatch import resolution across WASM entry points#1208

Open
marshacb wants to merge 17 commits into
mainnetfrom
feat/cam-dynamic-dispatch-imports
Open

[Feature] Add dynamic dispatch import resolution across WASM entry points#1208
marshacb wants to merge 17 commits into
mainnetfrom
feat/cam-dynamic-dispatch-imports

Conversation

@marshacb

@marshacb marshacb commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Motivation

Programs using call.dynamic reference their targets via field-encoded identifiers at runtime, not through static import declarations. The existing resolve_imports only loads statically declared imports, so dynamically dispatched targets would never be added to the process which would cause authorization and execution to fail for any program using call.dynamic.

This PR:

  • Unifies import resolution into a single resolve_imports_or_builder dispatcher across all WASM entry points (execute, authorize, proving request, and deploy paths)
  • Introduces ProgramImportsBuilder, a zero-cost WASM builder that carries proving/verifying keys alongside program source, with programNames()/getProgram() for lightweight enumeration and toObject()/fromObject() for full round-trip serialization including keys as Uint8Array
  • Adds a KeyStore interface for persistent key storage, with automatic load-before-execution (loadKeysFromStore) and save-after-execution (persistExtractedKeys) so the second execution of a program skips key synthesis entirely
  • Includes IndexedDBKeyStore, a browser-native KeyStore implementation for the React template
  • Handles the new Literal::Identifier variant from the latest snarkVM rev

Test Plan

WASM tests (Rust):

  • test_resolve_dynamic_imports - verifies resolve_imports does NOT load dynamic targets, and resolve_dynamic_imports does
  • test_resolve_multiple_dynamic_imports - multiple dynamic targets loaded from a single imports object
  • test_authorize_dynamic_dispatch - process.authorize() succeeds for a call.dynamic program after import resolution
  • test_program_imports_program_names - lightweight enumeration without key serialization
  • test_program_imports_get_program - source retrieval for known/unknown programs
  • test_program_imports_add_key_bytes_* - byte-level key round-trip, validation of missing program, and invalid bytes
  • test_program_imports_to_object_from_object_roundtrip_keys - full structured format round-trip with real VK bytes

SDK tests (TypeScript):

  • Dynamic dispatch tests - fixture parsing, offline execution with single/multiple dynamic targets, authorization with serialization round-trip, delegated proving path
  • KeyStore integration tests - buildProgramImports with static/dynamic imports and network merge, loadKeysFromStore positive/negative/error paths, persistExtractedKeys with top-level keys, resolveTopLevelKeys fallback chain, synthesizeKeys auto-persist, run() end-to-end with auto-built builder and post-execution persistence

Manual testing:

  • Node template (template-node-ts) updated to use LocalFileKeyStore with setKeyStore() for persistent key caching on disk
  • React template (template-react-ts) with IndexedDBKeyStore - verified keys persist to IndexedDB after first execution, second execution logs Inserting externally provided proving and verifying keys and skips synthesis

Note

Medium Risk
Touches core WASM ProgramManager execution/authorization/deploy/proving-request paths; incorrect import loading or clone semantics could break transaction building for existing consumers.

Overview
Adds ProgramManager::resolve_dynamic_imports to load non-statically-imported programs from the user-provided imports map, enabling programs that use call.dynamic to authorize/execute successfully.

Wires this dynamic import resolution (and associated imports.clone() adjustments) into the WASM ProgramManager entry points for authorization, execution (offline/on-chain/devnode/fee estimation), deployment/upgrade, and proving requests.

Expands e2e and SDK test coverage with simple call.dynamic programs (single + multiple targets) and new tests for ProgramManager.run(), buildAuthorization/buildAuthorizationUnchecked, provingRequest, plus WASM unit tests validating dynamic import loading behavior.

Written by Cursor Bugbot for commit af672cd. This will update automatically on new commits. Configure here.

@marshacb marshacb changed the title feat: Add dynamic dispatch import resolution across all WASM entry po… feat: Add dynamic dispatch import resolution across WASM entry points Feb 26, 2026
@marshacb marshacb force-pushed the feat/cam-dynamic-dispatch-imports branch from 4c5615d to ae46449 Compare February 26, 2026 06:13
@marshacb marshacb force-pushed the feat/cam-dynamic-dispatch-imports branch from ae46449 to 15acf8e Compare February 26, 2026 06:36
@marshacb marshacb force-pushed the feat/cam-dynamic-dispatch-imports branch from 15acf8e to af672cd Compare February 26, 2026 15:07
@marshacb marshacb marked this pull request as ready for review February 26, 2026 15:34
@marshacb

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for dynamic dispatch (call.dynamic) import resolution in WASM entry points. Programs using call.dynamic reference targets via field-encoded identifiers at runtime rather than static import declarations, so the existing resolve_imports function did not load these dynamically dispatched targets. This PR introduces resolve_dynamic_imports to load all programs from the user-provided imports object that aren't already in the process, enabling authorization and execution to succeed for programs using call.dynamic.

Changes:

  • Adds resolve_dynamic_imports function to load all programs from imports object not already in the process
  • Integrates dynamic import resolution into 14 WASM entry points across execute, authorize, deploy, and proving request paths
  • Adds comprehensive test coverage with simple dynamic dispatch programs and multi-target scenarios

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
wasm/src/programs/manager/mod.rs Implements resolve_dynamic_imports function and adds test programs and unit tests
wasm/src/programs/manager/proving_request.rs Adds dynamic import resolution to proving request path
wasm/src/programs/manager/execute.rs Adds dynamic import resolution to 6 execution entry points and authorization test
wasm/src/programs/manager/deploy.rs Adds dynamic import resolution to 5 deployment/upgrade entry points
wasm/src/programs/manager/authorize.rs Adds dynamic import resolution to authorization entry points
sdk/tests/dynamic-dispatch.test.ts Adds SDK tests for authorization and proving request with dynamic dispatch
sdk/tests/data/dynamic-dispatch.ts Adds test program constants and field-encoded identifiers
e2e/dynamic/index.js Adds e2e execution tests for single and multiple dynamic imports

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@marshacb marshacb changed the title feat: Add dynamic dispatch import resolution across WASM entry points [Feature]: Add dynamic dispatch import resolution across WASM entry points Mar 2, 2026
@marshacb marshacb changed the title [Feature]: Add dynamic dispatch import resolution across WASM entry points [Feature] Add dynamic dispatch import resolution across WASM entry points Mar 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread sdk/src/program-manager.ts Outdated
Comment thread wasm/src/programs/manager/execute.rs Outdated
Comment thread wasm/src/programs/manager/mod.rs
Comment thread wasm/src/programs/manager/mod.rs
Comment thread wasm/src/programs/manager/mod.rs
Comment thread wasm/src/programs/manager/imports.rs Outdated
Comment thread sdk/src/program-manager.ts

@iamalwaysuncomfortable iamalwaysuncomfortable left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a great first start, but I believe we need to add a few more things here.

Namely:

  1. We want to add an option at the top level for people to send in program imports that include the proving and verifying keys into the top level of calls of the program manager.
  2. We want to add the ability for keys to be resolved via any object implementing the KeyProvider interface so if keys have been stored, it can retrieve them prior to any call.
  3. In resolve_program imports recursion we probably want to ensure we're adding in any proving and verifying keys stored.

Comment thread wasm/src/programs/manager/imports.rs
Comment thread wasm/src/programs/manager/imports.rs Outdated
#[wasm_bindgen]
#[derive(Clone)]
pub struct ProgramImports {
entries: HashMap<String, ProgramEntry>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
entries: HashMap<String, ProgramEntry>,
entries: HashMap<ProgramIDNative, ProgramEntry>,

We want to store the ProgramID here instead I think.

Comment thread sdk/src/models/imports.ts
program?: string | Program;
imports?: ProgramImports;
edition?: number,
programImportsBuilder?: ProgramImportsBuilder;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could allow this as an option parameter for flexibility, but MOSTLY construct keys inside the ProgramManager functions.

Comment thread sdk/src/program-manager.ts
Comment on lines +173 to +181
/// Check whether a specific program has been added.
///
/// @param {string} name The program name.
/// @returns {boolean}
#[wasm_bindgen(js_name = "hasProgram")]
pub fn has_program(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similarly here

Suggested change
/// Check whether a specific program has been added.
///
/// @param {string} name The program name.
/// @returns {boolean}
#[wasm_bindgen(js_name = "hasProgram")]
pub fn has_program(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
}
/// Check whether a specific program has been added.
///
/// @param {string} name The program name.
/// @returns {boolean}
#[wasm_bindgen(js_name = "contains")]
pub fn contains(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
}

Comment thread wasm/src/programs/manager/imports.rs Outdated
Comment on lines +183 to +195
/// Extract source code from a JS value (plain string or object with `.source`).
///
/// Supports both legacy format (`"source code"`) and structured format
/// (`{ source: "source code", ... }`).
pub(crate) fn extract_source(value: &Option<wasm_bindgen::JsValue>) -> Option<String> {
let value = value.as_ref()?;
// Plain string format: "source code"
if let Some(source) = value.as_string() {
return Some(source);
}
// Structured object format: { source: "source code", ... }
Reflect::get(value, &"source".into()).ok().and_then(|v| v.as_string())
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rename to program just to stick with existing conventions

Suggested change
/// Extract source code from a JS value (plain string or object with `.source`).
///
/// Supports both legacy format (`"source code"`) and structured format
/// (`{ source: "source code", ... }`).
pub(crate) fn extract_source(value: &Option<wasm_bindgen::JsValue>) -> Option<String> {
let value = value.as_ref()?;
// Plain string format: "source code"
if let Some(source) = value.as_string() {
return Some(source);
}
// Structured object format: { source: "source code", ... }
Reflect::get(value, &"source".into()).ok().and_then(|v| v.as_string())
}
/// Extract source code from a JS value (plain string or object with `.program`).
///
/// Supports both legacy format (`"source code"`) and structured format
/// (`{ program: "source code", ... }`).
pub(crate) fn extract_source(value: &Option<wasm_bindgen::JsValue>) -> Option<String> {
let value = value.as_ref()?;
// Plain string format: "source code"
if let Some(program) = value.as_string() {
return Some(program);
}
// Structured object format: { program: "source code", ... }
Reflect::get(value, &"source".into()).ok().and_then(|v| v.as_string())
}

Comment thread wasm/src/programs/manager/imports.rs Outdated
/// (respecting transitive static imports via depth-first resolution).
/// 2. Inserts any pre-provided proving and verifying keys directly into
/// the process, avoiding expensive on-demand key synthesis.
pub(crate) fn resolve_into(&self, process: &mut ProcessNative) -> Result<(), String> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we rename this as load_programs?

Suggested change
pub(crate) fn resolve_into(&self, process: &mut ProcessNative) -> Result<(), String> {
pub(crate) fn load_programs(&self, process: &mut ProcessNative) -> Result<(), String> {

Comment thread wasm/src/programs/manager/imports.rs Outdated
if !process.contains_program(program.id()) {
log(&format!("Importing program: {name}"));
// Resolve transitive static imports first.
self.resolve_program_imports(process, &program)?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like this method might skip adding the program keys, perhaps we want to ensure that when we're calling this, any keys stored are also added?

}

/// Recursively resolve a program's static imports in depth-first order.
fn resolve_program_imports(&self, process: &mut ProcessNative, program: &ProgramNative) -> Result<(), String> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We might want to add any program proving and verifying keys here or consider just unifying this method with the method that calls it, because I believe as is, if programs get added here, it wouldn't add any of the proving or verifying keys.

iamalwaysuncomfortable and others added 7 commits March 23, 2026 19:56
* Add WASM support for dynamic dispatch variant types (DynamicRecord, DynamicFuture, RecordWithDynamicID, ExternalRecordWithDynamicID)

* fmt

* fix(wasm): normalize dynamic dispatch type strings to match snarkVM serialization and add missing doc comments

* add JS/TS fixture tests for dynamic dispatch variant types (record_dynamic, record_with_dynamic_id, external_record_with_dynamic_id)
* Handle Consensus V14 in deployments

* Remove redundant .map_err in latest_stateroot call

* Update getOrInitConsensusVersionTestHeights tests and doc comments to handle all currently active test version heights

* Deallocate deployment cost tuple
* Add stringToField utility function for converting program and function names to fields for dynamic dispatch

* Add exports of the stringToField utility in the js SDK
* Add the dynamic record type

* Add JS side tests for the DynamicRecord type

* Check visibility on record conversion roundtrip

---------

Signed-off-by: Mike Turner <mike@provable.com>
d0cd and others added 10 commits March 23, 2026 18:11
* Handle Consensus V14 in deployments (#1230)

* Handle Consensus V14 in deployments

* Remove redundant .map_err in latest_stateroot call

* Update getOrInitConsensusVersionTestHeights tests and doc comments to handle all currently active test version heights

* Deallocate deployment cost tuple

* Updates to make AMM tests works

* Also handle upgrade

* Address feedback

* Cleanup

* Cargo fmt lints

* Revert .gitignore changes unrelated to this PR

* Change core rev to the testnet 4.6.0 candidate

* Bump version update to v0.9.18

* Remove unecessary dependencies in the workspace root

---------

Co-authored-by: Mike Turner <mike@provable.com>
…m to avoid key serialization, fix test stubs
@marshacb marshacb force-pushed the feat/cam-dynamic-dispatch-imports branch from dcc4c69 to 3774d9d Compare March 24, 2026 14:27
@iamalwaysuncomfortable iamalwaysuncomfortable force-pushed the feat/dynamic-dispatch branch 2 times, most recently from 6c5a46a to 486b002 Compare March 26, 2026 07:29
Base automatically changed from feat/dynamic-dispatch to testnet March 26, 2026 07:33
Base automatically changed from testnet to mainnet March 31, 2026 16:29
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.

4 participants