Skip to content

[Feat] Add On-Demand Bytecode Verification API - #272

Open
apenzk wants to merge 7 commits into
m1from
feat/package-registry-source-verification
Open

[Feat] Add On-Demand Bytecode Verification API#272
apenzk wants to merge 7 commits into
m1from
feat/package-registry-source-verification

Conversation

@apenzk

@apenzk apenzk commented Jan 5, 2026

Copy link
Copy Markdown

Add On-Demand Bytecode Verification API

Related Issues

Related to movement-network/explorer#114

Related Work

Explorer PR: movement-network/explorer#115

Description

This PR adds an on-demand bytecode verification feature to the Aptos Node API. The feature allows nodes (when enabled via a configuration flag) to verify that source code stored in PackageRegistry compiles to the same bytecode as what's deployed on-chain. Verification results are cached locally on the node to avoid re-verification on every request.

New API Endpoint: GET /v1/accounts/{address}/modules/{module_name}/verification_status

Status HTTP Code Response Meaning
Verified Match 200 {"verified": true} Source compiles to matching bytecode ✓
Verified Mismatch 200 {"verified": false} Source compiles but bytecode differs (SUSPICIOUS!)
Compilation Error 422 {"error_code": "compilation_error", "message": "..."} Cannot verify (e.g., unsupported dependencies)
No Source 404 {"error_code": "resource_not_found", ...} No source code in PackageRegistry
Disabled 503 {"error_code": "api_disabled", ...} Verification feature disabled on node

Configuration: api.bytecode_verification_enabled (default: false)

  • When enabled, nodes can perform on-demand verification
  • When disabled, verification endpoint returns HTTP 503

Caching: Verification results are cached using mini_moka LRU cache (10,000 entry limit) keyed by (address, module_name, upgrade_number). The upgrade_number from PackageMetadata ensures that package upgrades invalidate cached verification results.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Performance improvement
  • Refactoring
  • Dependency update
  • Documentation update
  • Tests

Which Components or Systems Does This Change Impact?

  • Validator Node
  • Full Node (API, Indexer, etc.)
  • Move/Aptos Virtual Machine
  • Aptos Framework
  • Aptos CLI/SDK
  • Developer Infrastructure
  • Other (specify)

How Has This Been Tested?

Unit Tests

Unit tests are included in api/src/tests/verification_test.rs covering:

  • Happy path: Source code matches bytecode → returns verified: true
  • Failure path: Source code doesn't match bytecode → returns verified: false
  • Disabled feature: Verification disabled → returns HTTP 503
  • Not found: No source code available → returns HTTP 404
  • Cache with upgrade_number: Verifies cache invalidation when package is upgraded
  • Framework dependencies: Verifies contracts using aptos_std:: compile correctly
  • Transitive dependencies: Verifies contracts using aptos_token:: (which requires aptos-framework transitively) compile correctly
  • Compilation failure: Verifies contracts with unsupported dependencies return CompilationError status (HTTP 422) and results are cached

Test packages are in api/src/tests/move/.

E2E Testing

A test setup is available at analysis-explorer-vulnerability that demonstrates:

  • Valid contract with matching source/bytecode
  • Invalid contract with mismatched source/bytecode

Key Areas to Review

  1. Verification Logic (api/src/verification.rs):

    • Compiles source code using Move compiler v2 with framework dependencies
    • Supports standard Aptos framework packages: move-stdlib, aptos-stdlib, aptos-framework, aptos-token, aptos-token-objects
    • Dynamically includes dependencies based on source code imports (with transitive dependency resolution)
    • Runs compilation in a dedicated thread with 16MB stack to handle large framework dependencies
    • Extracts named addresses from module declarations and maps them to deployer addresses
    • Compares bytecode by deserializing CompiledModule objects and clearing metadata before comparison
    • This approach ignores metadata differences that don't affect functional bytecode
  2. Caching Strategy (api/src/context.rs):

    • Uses mini_moka::sync::Cache with LRU eviction (10,000 entry limit)
    • Cache key: (AccountAddress, module_name, upgrade_number)
    • The upgrade_number from PackageMetadata ensures package upgrades invalidate cached results
    • Cache stores VerificationStatus enum internally, converts to boolean for API response
    • Cache is node-local (off-chain) to avoid consensus issues
  3. API Endpoint (api/src/state.rs):

    • Checks bytecode_verification_enabled flag before processing
    • Returns appropriate HTTP status codes:
      • 200: Verification completed (verified: true/false)
      • 422: Compilation error (can't verify, not a mismatch)
      • 404: No source code available
      • 503: Feature disabled
    • Supports both JSON and BCS response formats
  4. Configuration (config/src/config/api_config.rs):

    • New field bytecode_verification_enabled defaults to false (opt-in feature)
    • Can be enabled via node config YAML or override file

Limitations

Supported Dependencies: This verification feature supports contracts that depend only on standard Aptos framework packages:

Package Address Description
move-stdlib 0x1 Basic types (vector, string, signer) - always included
aptos-stdlib 0x1 Utilities (math, tables, string_utils)
aptos-framework 0x1 Core framework (coin, account, staking)
aptos-token 0x3 Original NFT token standard
aptos-token-objects 0x4 New token objects standard

Unsupported: Contracts that depend on other user-deployed packages (e.g., use alice::library) cannot be verified, as we don't have access to those dependencies at compile time. In such cases, the API returns HTTP 422 with error_code: "compilation_error" - this is distinct from verified: false (which indicates a verified mismatch and is suspicious).

Checklist

  • I have read and followed the CONTRIBUTING doc
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I identified and added all stakeholders and component owners affected by this change as reviewers
  • I tested both happy and unhappy path of the functionality
  • I have made corresponding changes to the documentation

Configuration

To enable bytecode verification, add to your node configuration:

api:
  bytecode_verification_enabled: true

For Movement testnet, create an override file at .movement/testnet_config_override.yaml:

# Enable bytecode verification API
api:
  bytecode_verification_enabled: true

Then start the node with the --config-override flag pointing to this file.

apenzk added 3 commits January 5, 2026 09:31
Add configurable bytecode verification that allows nodes to verify source
code stored in PackageRegistry compiles to matching on-chain bytecode.

Changes:
- Add api.bytecode_verification_enabled config flag (default: disabled)
- Add GET /accounts/{address}/modules/{module_name}/verification_status endpoint
- Implement on-demand verification with in-memory caching (10k entry LRU)
- Add VerificationStatus enum for internal caching (VerifiedSuccess/Failure/Unverified)
- API returns boolean: true (verified), false (verification failure), 404 (no source), 503 (disabled)
- Verification is node-local, on-demand, and does not affect consensus

The endpoint performs verification when requested by clients (e.g., explorers),
caches results to avoid re-compilation, and returns verification status.
When the flag is disabled, the endpoint returns 503 Service Unavailable.
@apenzk
apenzk changed the base branch from movement to l1-migration January 5, 2026 16:09

@ganymedio ganymedio 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.

@apenzk looking good; tested it with the explorer and it works for the simple examples.

Raised a few issues; see what you think.

Would also like a variety of contracts requiring different deps than only std, in https://github.com/movementlabsxyz/analysis-explorer-vulnerability

Comment thread api/src/tests/verification_test.rs Outdated
Comment on lines +91 to +92
/// Tests verification returns HTTP 503 when feature is disabled.
/// Why: Ensures feature flag correctly gates access.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
/// Tests verification returns HTTP 503 when feature is disabled.
/// Why: Ensures feature flag correctly gates access.
/// Tests verification returns HTTP 503 when feature is disabled.

Comment thread api/src/tests/verification_test.rs Outdated
Comment on lines +49 to +50
/// Tests verification returns `false` when source doesn't match bytecode.
/// Why: Core security feature - detecting mismatched source code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
/// Tests verification returns `false` when source doesn't match bytecode.
/// Why: Core security feature - detecting mismatched source code.
/// Tests verification returns `false` when source doesn't match bytecode.

Comment thread api/src/tests/verification_test.rs Outdated
Comment on lines +29 to +30
/// Tests verification returns `true` when source matches bytecode.
/// Why: Core happy path validation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
/// Tests verification returns `true` when source matches bytecode.
/// Why: Core happy path validation.
/// Tests verification returns `true` when source matches bytecode.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Think the four tests' "why" comments are redundant / over-stating the obvious.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

removed

Comment thread api/src/tests/verification_test.rs Outdated
Comment on lines +111 to +112
/// Tests verification returns HTTP 404 for non-existent module.
/// Why: Proper error handling for invalid queries.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
/// Tests verification returns HTTP 404 for non-existent module.
/// Why: Proper error handling for invalid queries.
/// Tests verification returns HTTP 404 for non-existent module.

Comment thread api/src/context.rs Outdated
}
}

pub fn get(&self, address: &AccountAddress, module_name: &str) -> Option<VerificationStatus> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@apenzk think the cache needs to include upgrade_number from PackageMetadata, otherwise the cache will have outdated stale versions of contracts even after they are upgraded.

      pub fn get(&self, address: &AccountAddress, module_name: &str, upgrade_number: u64) -> Option<VerificationStatus> {
          self.cache.get(&(*address, module_name.to_string(), upgrade_number))
      }

      pub fn insert(&self, address: AccountAddress, module_name: String, upgrade_number: u64, status: VerificationStatus) {
          self.cache.insert((address, module_name, upgrade_number), status);
      }
  }

If you go with this approach, update the cache type at line 77:

  pub struct VerificationCache {
      cache: Cache<(AccountAddress, String, u64), VerificationStatus>,
  }

Then the callers in state.rs would need to pass upgrade_number from the PackageMetadata.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

you are correct

i fix this now in commit ea9903a

I also add a test case in https://github.com/movementlabsxyz/analysis-explorer-vulnerability that checks that the explorer displays the upgraded contract.

Comment thread api/src/state.rs Outdated
if let Some(cached_status) = self
.context
.verification_cache()
.get(&address_inner, &module_name_str)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if you include upgrade_number, seems like you'll need to fetch PackageRegistry first and do something like

let upgrade_number = registry.packages.iter()
      .find(|pkg| pkg.modules.iter().any(|m| m.name == module_name_str))
      .map(|pkg| pkg.upgrade_number)
      .unwrap_or(0);

before calling get and insert.

Comment thread api/src/verification.rs Outdated
drop(file);

// Get minimal framework dependency - just move-stdlib for basic types like string
// Keep it minimal to avoid stack overflow from processing large frameworks

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wouldn't this be insufficient for anything other than the simplest contracts? In deps don't you need something more like

     // 0x1 - core framework
      aptos_framework::path_in_crate("move-stdlib/sources").to_string_lossy().to_string(),
      aptos_framework::path_in_crate("aptos-stdlib/sources").to_string_lossy().to_string(),
      aptos_framework::path_in_crate("aptos-framework/sources").to_string_lossy().to_string(),
      // 0x3 - token standard
      aptos_framework::path_in_crate("aptos-token/sources").to_string_lossy().to_string(),
      // 0x4 - token objects
      aptos_framework::path_in_crate("aptos-token-objects/sources").to_string_lossy().to_string(),

and in named addresses also include format!("aptos_token_objects=0x4"), ?

Did you encounter stack overflow issues?

@apenzk apenzk Jan 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Wouldn't this be insufficient for anything other than the simplest contracts?

you are 100% correct.

fixing this ( see commit 03fae01) turned out to be quite tricky. i had to
A) add support for the above frameworks
B) increase the COMPILATION_STACK_SIZE to overcome stack overflow issues
C) cache also cases where compilation did not work
D) add following limiation: unfortunatelly for now i limit to the above deps and additional deps throw an error for when it cannot be compiled

@ganymedio ganymedio Jan 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@apenzk if a dependency of alice:: produces a warning then I think there will be too many such cases to consider this system complete enough to be useful. I can potentially try to give some time to working out a more complete solution after the super app release, and will be happy to review further iterations.

Comment thread api/src/verification.rs
match self {
VerificationStatus::VerifiedSuccess => Some(true),
VerificationStatus::VerifiedFailure => Some(false),
VerificationStatus::Unverified => None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Perhaps there should be another status like VerificationError for when verifier couldn't recompile (missing deps, tooling issue)?

@apenzk apenzk Jan 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

great idea. i have added error 422, which handles this now

see commit 03fae01

- Update VerificationCache to use (address, module_name, upgrade_number) as cache key
- Fetch PackageRegistry before cache lookup to extract upgrade_number
- Update cache.get() and cache.insert() calls to include upgrade_number parameter
- Add comprehensive test for upgrade_number handling in cache
- Ensures each package version is verified independently
@apenzk

apenzk commented Jan 14, 2026

Copy link
Copy Markdown
Author

@andygolay i have added

  • added support for contracts being updated
  • compilation of contracts that use more than std lib, but i am limitting it to a few default lib for now - which should cover many cases. (See also the Limitation section in the PR description)
  • For the case where it cannot be compiled a new type of error message is delivered to the explorer (and displayed accordingly)

I also added two cases in analysis-explorer-vulnerability to test correct behavior

@apenzk
apenzk requested a review from ganymedio January 14, 2026 12:35
- Replace ledger_version query param with upgrade_number
- Always use latest state; read current upgrade_number from PackageRegistry
- If client's upgrade_number != current, return 404 and do not serve from cache
- Cache key: (address, module_name, upgrade_number)

PackageRegistry only holds latest package metadata, so we cannot verify at
an arbitrary ledger version. upgrade_number identifies the contract version
and lets us reject stale clients so we never return verification for the
wrong package version.

@ganymedio ganymedio 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.

Let me know if this comment misinterprets the state of the system #272 (comment)

I generally can't approve unless the system is complete. I don't think it's fair to show a warning on contracts this system can't verify as it will likely cause end user confusion and FUD toward the modules with such a warning displayed in the explorer. I'll try to give some time to consider how to make the system complete.

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.

2 participants