npm package: @scallop-io/sui-move-bytecode-verifier
Description: Tool to verify Sui Move on-chain packages
Author: team@scallop.io
Latest version: 0.1.0

# Sui Move Bytecode Verifier

A CLI tool to verify Sui Move bytecodes across different sources: local directories, GitHub repositories, on-chain packages, and publish/upgrade transactions. It compiles source, fetches bytecodes, and compares them byte-for-byte.

## Prerequisites

- **Node.js** >= 18
- **Sui CLI** installed ([install guide](https://docs.sui.io/guides/developer/getting-started/sui-install))
- **Git**
- (Optional) **suiup** for automatic compiler version switching

## Setup

```bash
git clone <this-repo>
cd contract-verifier
npm install
```

### Build (optional)

```bash
npm run build
```

This produces `dist/index.js` which can be run directly or linked globally:

```bash
npm link
sui-move-bytecode-verifier --help
```

## Usage

The `verify` command compares bytecodes from **any two sources**. Specify exactly two of: `--local`, `--repo`, `--on-chain`, `--tx-bytes`.

### 1. Local vs On-chain

Check if your local Move source code matches an on-chain package:

```bash
npx tsx src/index.ts verify \
  --local ./my-package \
  --on-chain 0x1234...abcd \
  --network mainnet
```

### 2. Local vs GitHub

Check if your local code matches a specific version in a GitHub repo:

```bash
npx tsx src/index.ts verify \
  --local ./my-package \
  --repo https://github.com/example-org/protocol \
  --ref v1.0.0
```

### 3. GitHub vs On-chain

Verify that a GitHub repo's source matches the on-chain deployed package:

```bash
npx tsx src/index.ts verify \
  --repo https://github.com/example-org/protocol \
  --ref v1.0.0 \
  --on-chain 0x1234...abcd \
  --network mainnet
```

### 4. Transaction bytes vs GitHub

Verify that the module bytes in a publish/upgrade transaction match a GitHub repo. This lets you check what you're about to sign **before** submitting:

```bash
# From a base64 file (e.g. saved from sui client or SDK)
npx tsx src/index.ts verify \
  --tx-bytes ./unsigned-tx.base64 \
  --repo https://github.com/example-org/protocol \
  --ref v1.0.0

# Or pass base64 inline
npx tsx src/index.ts verify \
  --tx-bytes "AAAC..." \
  --repo https://github.com/example-org/protocol \
  --ref v1.0.0
```

The `--tx-bytes` flag accepts either a **file path** (containing base64-encoded transaction bytes) or an **inline base64 string**. The tool parses the BCS-encoded `TransactionData`, finds the `Publish` or `Upgrade` command, and extracts the module bytecodes for comparison.

### Source flags

| Flag | Description |
|---|---|
| `--local <path>` | Local Move package directory |
| `-r, --repo <url>` | GitHub repository URL (use with `--ref` and optionally `--path`) |
| `--on-chain <id>` | On-chain Sui package object ID (0x...) |
| `--tx-bytes <base64\|file>` | Transaction bytes: inline base64 or path to a file containing base64 |

### Other options

| Option | Description | Default |
|---|---|---|
| `--ref <ref>` | Git ref for `--repo`: branch, tag, or commit hash | `main` |
| `--path <path>` | Path to Move package within repo | auto-detect |
| `-n, --network <network>` | `mainnet`, `testnet`, or `devnet` | `mainnet` |
| `--rpc-url <url>` | Custom RPC endpoint (overrides --network) | |
| `--sui-version <version>` | Override Sui CLI version for compilation | from Move.lock |
| `--no-version-check` | Skip compiler version matching | |
| `-o, --output <format>` | `table` or `json` | `table` |
| `-v, --verbose` | Show build logs and byte diff details | |
| `--no-cache` | Skip cache, always re-clone and re-build | |
| `--cache-dir <dir>` | Custom cache directory | `~/.cache/sui-verify` |

### Batch verification

Verify multiple packages at once using a YAML config file:

```bash
npx tsx src/index.ts verify-batch --config packages.yaml
```

**packages.yaml:**

```yaml
network: mainnet
packages:
  - id: "0x1234...abcd"
    repo: "https://github.com/example-org/protocol"
    ref: "v1.0.0"
    path: "contracts/core"
  - id: "0x5678...ef01"
    repo: "https://github.com/example-org/token"
    ref: "main"
```

### Cache management

Cloned repositories are cached to speed up repeated verifications. Cached repos automatically expire after **7 days** — on the next run, expired caches are purged and the repo is re-cloned fresh. On-chain bytecode is cached indefinitely since Sui packages are immutable.

Use `--no-cache` to bypass the cache entirely for a single run.

```bash
# Show cache size and location
npx tsx src/index.ts cache info

# Clear all cached data
npx tsx src/index.ts cache clean
```

## How it works

### Overview

The tool resolves each of the two sources into a set of module bytecodes, then compares them:

| Source | How it resolves |
|---|---|
| `--local` | Builds the local Move package via `sui move build`, collects `.mv` files |
| `--repo` | Clones the GitHub repo at the specified ref, then builds like `--local` |
| `--on-chain` | Fetches the package object from Sui RPC, extracts each module's bytecode |
| `--tx-bytes` | Parses BCS transaction bytes locally, finds the Publish/Upgrade command, extracts module bytes |

When one of the sources is `--on-chain`, address substitution is applied: the compiled bytecode's zero-address placeholder is replaced with the real package address before comparison (matching Sui CLI's `verify-source` behavior).

Steps for a typical GitHub-vs-on-chain verification:
1. **Fetch on-chain bytecode** via Sui RPC
2. **Clone repo** at the specified ref (cached, 7-day TTL)
3. **Detect compiler version** from `Move.lock`, switch via `suiup` if needed
4. **Prepare source** — rewrite self-address to `0x0` in `Move.toml`
5. **Build** via `sui move build`, collect `.mv` files
6. **Address substitution & compare** — substitute real address into compiled bytecode's `address_identifiers` table, compare byte-for-byte
7. **Report** with per-module match/mismatch classification

### How bytecode comparison works (in detail)

The comparison is **not** just a size check — it is a full **byte-for-byte comparison** of the entire bytecode content for every module. The approach follows the same logic as the official Sui CLI's `verify-source` command. Here is exactly what happens:

#### The address problem

When you compile a Move package locally, the self-address in `Move.toml` is set to `"0x0"`. This means the compiled `.mv` bytecode files contain `0x0000...0000` (32 zero bytes) in the module's `address_identifiers` table where the package references its own address.

When that same package is published on-chain, Sui replaces the zero-address placeholder with the actual package object ID. So the on-chain bytecode contains the real address (e.g. `0xabcd...1234`) in those same positions.

If we compared them directly, they would always mismatch — even if the source code is identical.

#### The address substitution step

To solve this, the tool **substitutes** the real package address into the compiled bytecode before comparing — the same approach the official Sui CLI uses:

1. Parse the compiled `.mv` bytecode binary header to locate the `address_identifiers` table. Move bytecode has a well-defined binary layout:
   - 4 bytes magic (`0xA11CEB0B`)
   - 4 bytes version (u32 little-endian)
   - Table count (ULEB128)
   - Table headers: each has a kind byte + offset + count (ULEB128-encoded)
   - Table data sections follow
2. Find the table with kind `5` (ADDRESS_IDENTIFIERS). Each entry is exactly 32 bytes (an `AccountAddress`).
3. For each entry in that table that is the zero-address (`0x00...00`), replace it with the real package address.
4. The result is a "substituted" compiled bytecode that should now be identical to the on-chain bytecode.

```
Compiled bytecode:           [..., address_identifiers: [0x0000...0000, 0x0000...0002], ...]
                                                         ^^^^^^^^^^^^^
                                                         self-address = 0x0 → substitute real address
                                                         vvvvvvvvvvvvv
After substitution:          [..., address_identifiers: [0xabcd...1234, 0x0000...0002], ...]
On-chain bytecode:           [..., address_identifiers: [0xabcd...1234, 0x0000...0002], ...]
                             ✔ exact match
```

This is more precise than doing a blanket find-and-replace across the entire bytecode. It only touches entries in the `address_identifiers` table, so it will never accidentally replace bytes that happen to be zero elsewhere in the bytecode (e.g. in instruction operands or string data).

#### Why this matches the Sui CLI approach

The official Sui CLI's `verify-source` (implemented in the `sui-source-validation` crate) does the same thing:

1. Compiles local source (self-address = `0x0`)
2. Deserializes the compiled bytecode into a `CompiledModule` struct
3. Locates the self-address entry in the `address_identifiers` table
4. Substitutes `0x0` → real package address
5. Compares the result against the on-chain deserialized `CompiledModule` using structural equality

Our tool does the equivalent in TypeScript: we parse the binary header to find the `address_identifiers` table and perform the same substitution, then compare the raw bytes directly. The Sui CLI operates on deserialized Rust structs; we operate on the raw binary — but since Move bytecode serialization is deterministic, the result is the same.

#### The comparison

After address substitution, the tool compares module-by-module:

1. **Module name matching** — Check that both sides have the exact same set of module names. If the on-chain package has a module `lending` but the compiled source does not (or vice versa), that is a mismatch.

2. **Byte-for-byte comparison** — For each module present in both, compare the substituted compiled bytes against the on-chain bytes at every single byte position. Both the length and content must be identical. Even a single byte difference means the module does not match.

3. **Diff reporting** — If a module does not match, the tool records the byte offsets where differences occur (up to 20). With `--verbose`, these are shown to help diagnose the cause (e.g. a different compiler version typically produces differences throughout the bytecode, while a source code change produces localized differences).

#### What this proves

If all modules pass the byte-for-byte comparison:
- The on-chain bytecode was compiled from the **exact same source code** at the given git ref.
- Using the **same compiler version** and **same dependencies**.
- No code was added, removed, or modified between the source and what was published.

If any module fails:
- The source code may have been modified after the commit that was published.
- A different compiler version may have been used (different versions produce different bytecode even from identical source).
- Dependencies may have changed (different dependency versions produce different bytecode).

#### Why only the self-address is substituted

Dependency addresses (e.g. `0x2` for Sui Framework) are the same in both compiled and on-chain bytecode — they are real published addresses specified in `Move.toml`. Only the package's own address differs between compilation (`0x0`) and on-chain (real address), so that is the only address that needs substitution. The tool specifically targets zero-address entries in the `address_identifiers` table and leaves all other addresses untouched.

## CI/CD integration

The CLI uses exit codes for automation:

| Exit code | Meaning |
|---|---|
| `0` | All modules verified |
| `1` | One or more modules failed verification |
| `2` | Error (invalid input, network failure, build failure) |

Use `--output json` for machine-readable output:

```bash
npx tsx src/index.ts verify 0x1234...abcd \
  --repo https://github.com/example-org/protocol \
  --ref v1.0.0 \
  --output json
```

## Private repositories

Set the `GITHUB_TOKEN` environment variable to clone private repos:

```bash
GITHUB_TOKEN=ghp_xxx npx tsx src/index.ts verify ...
```

## Troubleshooting

### Compiler version mismatch

If verification fails, the most common cause is a compiler version mismatch. The tool reads the expected version from `Move.lock` and attempts to switch automatically via `suiup`. If that fails:

```bash
# Install the required version manually
suiup install sui@1.55.0

# Or skip version checking (may produce false negatives)
npx tsx src/index.ts verify ... --no-version-check
```

### Multiple Move packages in repository

If the repo contains multiple `Move.toml` files, specify which package to verify:

```bash
npx tsx src/index.ts verify ... --path contracts/core
```

### Build failures

Ensure all dependencies in `Move.toml` are accessible. Local path dependencies must be contained within the repository. Git dependencies must point to valid URLs and revisions.

## Using as a library

Install as a dependency in your project:

```bash
npm install sui-move-bytecode-verifier
```

### High-level: full verification

```typescript
import { verifyPackage } from "sui-move-bytecode-verifier";

const result = await verifyPackage({
  sourceA: { kind: "repo", url: "https://github.com/org/repo", ref: "v1.0.0" },
  sourceB: { kind: "onchain", packageId: "0xabc..." },
  network: "mainnet",
  noVersionCheck: false,
  verbose: false,
  noCache: false,
  cacheDir: "/tmp/sui-verify-cache",
});

console.log(result.verified); // true or false
console.log(result.comparison.moduleResults); // per-module details
```

### Low-level: individual steps

Use the building blocks directly for custom pipelines:

```typescript
import {
  createSuiClient,
  fetchOnChainPackage,
  buildPackage,
  compareModules,
  extractTxModules,
  prepareForVerification,
  parseMoveLock,
} from "sui-move-bytecode-verifier";

// Fetch on-chain bytecodes
const client = createSuiClient("mainnet");
const onChainPkg = await fetchOnChainPackage("0xabc...", client);

// Build a local package
prepareForVerification("./my-pkg/Move.toml", "0xabc...");
const buildResult = await buildPackage("./my-pkg");

// Compare
const result = compareModules(
  buildResult.modules,
  onChainPkg.moduleMap,
  "0xabc...",
);

console.log(result.match);
for (const mod of result.moduleResults) {
  console.log(`${mod.name}: ${mod.mismatchKind}`);
}

// Extract modules from unsigned transaction bytes
const txModules = extractTxModules(base64TxBytes);

// Parse Move.lock for compiler version
const lockInfo = parseMoveLock("./my-pkg/Move.lock");
console.log(lockInfo?.compilerVersion);
```

### Exported API

| Export | Description |
|---|---|
| `verifyPackage(options)` | High-level: resolve two sources, compare, return result |
| `fetchOnChainPackage(id, client)` | Fetch package bytecodes from Sui RPC |
| `extractTxModules(base64Bytes)` | Extract module bytecodes from BCS transaction bytes |
| `buildPackage(path, suiBinary?)` | Run `sui move build` and collect `.mv` files |
| `compareModules(a, b, packageId?)` | Compare two module maps with mismatch classification |
| `cloneAndCheckout(url, ref, cacheDir, noCache)` | Clone a git repo with caching |
| `parseMoveToml(path)` | Parse a `Move.toml` file |
| `prepareForVerification(path, packageId)` | Rewrite self-address to `0x0` for compilation |
| `parseMoveLock(path)` | Parse `Move.lock` for compiler version |
| `createSuiClient(network, rpcUrl?)` | Create a `SuiClient` for a network |
| `ensureSuiVersion(version, noCheck)` | Match sui CLI version via suiup |
| `setVerbose(enabled)` | Enable/disable verbose logging |
| Error classes | `VerifierError`, `NetworkError`, `BuildError`, `ConfigError`, etc. |

## Development

```bash
# Run tests
npm test

# Type check
npm run typecheck

# Build library + CLI
npm run build

# Run CLI in dev mode
npx tsx src/index.ts verify --help
```