Solana Anchorのデバッグ方法

Solanaの開発ツール Anchor のデバッグ方法。

現象

$ anchor test を実行しても、Rustのデバッグ情報(dbgやprintln, msgなど)がターミナル上に出力できない。

原因

おそらく「$ anchor test」は、デプロイしたあとに、Anchor.tomlに記述されている「test = "yarn run mocha -t 1000000 tests/"」を実行してると思われる。
そのため、Rustのデバッグ情報はターミナル上には出力されず、JS側のデバッグ情報だけ出力されているっぽい。

Prgoram(Rust)のデバッグ対策

(案1)ブロックチェーンのプログラムログに出力

Rustの「msg!」を記述しておくと、ブロックチェーンのProgram Logsに出力できる。

「$ anchor test」はbuild→deploy→testまで実行され、「$ anchor test」を実行したディレクトリに「.anchor/program-logs/[declare_idのアドレス]」が出力され、そこでデプロイログが確認できる。デフォルトがlocalnetのため、気軽に実行可能。

たとえば、以下のように「msg!("msg here!!");」を記述すると、固定文字が出力され、「msg!("{:?}", 変数);」を記述すると、変数が出力される。

lib.rs

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
mod basic_0 {
    use super::*;
    let variable = 123456789;
    pub fn initialize(_ctx: Context<Initialize>) -> ProgramResult {
        msg!("msg here!!");
        msg!("{:?}", variable);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

ターミナルでanchor testすると、Program Logに「msg here!!」と「変数(今回のケースだと123456789)」が出力される。

% pwd
/Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0


% anchor test
BPF SDK: /Users/user/.local/share/solana/install/releases/1.8.5/solana-release/bin/sdk/bpf
cargo-build-bpf child: rustup toolchain list -v
cargo-build-bpf child: cargo +bpf build --target bpfel-unknown-unknown --release
    Finished release [optimized] target(s) in 0.26s
cargo-build-bpf child: /Users/user/.local/share/solana/install/releases/1.8.5/solana-release/bin/sdk/bpf/dependencies/bpf-tools/llvm/bin/llvm-readelf --dyn-symbols /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0.so

To deploy this program:
  $ solana program deploy /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0.so
The program address will default to this keypair (override with --program-id):
  /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0-keypair.json
yarn run v1.22.17
$ /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/node_modules/.bin/mocha -t 1000000 tests/


  basic-0
    ✔ Uses the workspace to invoke the initialize instruction (362ms)


  1 passing (365ms)

✨  Done in 5.54s.


% cat .anchor/program-logs/Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS.basic_0.log
Streaming transaction logs mentioning Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS. Confirmed commitment
Transaction executed in slot 2:
  Signature: 4Jd5s5dnEWEuEbUkTdwxNULiDwDvV71VZhsE6C1Xp4Wr3MbnEUJhRWHGomWTeNSUFCc3SXCTu3TNcBsaG2QgUiZu
  Status: Ok
  Log Messages:
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]
    Program log: Instruction: Initialize
    Program log: msg here!!
    Program log: 123456789
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 396 of 200000 compute units
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS success

(参考)なお、Solana Explorerだと最下部にProgram Logsが確認できる → Solana Explorer Devnet

(案2)cargo testで出力

「$ cargo test」が利用できるため、普通にRustでtestを記述する方法。
Anchor tutorial basic-0 を使った記述例。

lib.rs

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
mod basic0 {
    use super::*;
    pub fn initialize(_ctx: Context<Initialize>) -> ProgramResult {
        Ok(())
    }
}

pub fn greeting() {
    let reply = "hello";
    dbg!(reply);
}

#[derive(Accounts)]
pub struct Initialize {}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_sanity() {
        greeting();

        let exp = "smile";
        dbg!(exp);
    }
}

Anchorのサンプルをそのままcargo testすると、「ambiguous name error[E0659]: basic_0 is ambiguous (name vs any other name during import resolution)」と怒られるため、リネームが必要。「basic_0 → basic0」のリネームでOK。

ターミナルで実行

% cargo test -- --nocapture
    Finished test [unoptimized + debuginfo] target(s) in 0.07s
     Running unittests (target/debug/deps/basic_0-a269b2199b5b3dd4)

running 2 tests
[programs/basic-0/src/lib.rs:15] reply = "hello"
[programs/basic-0/src/lib.rs:30] exp = "smile"
test test_id ... ok
test test::test_sanity ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests basic_0

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

(案3)localnetでSolana Explorerに出力

anchorプログラムをlocalnetにデプロイしてから、Solana Explorerにアクセスする方法。Solana ExplorerでアカウントもあわせてGUI上でトレースできるため便利。

localhostに設定:

% solana config set --url localhost

ローカルのバリデータを起動:

% solana-test-validator

デプロイしてProgram Idを取得:

% anchor deploy
~~~
Program Id: EXi7tnp3oYjUvpoDm3Bxc63KhYKS1nJW5xa8zyuZBESw
~~~

Anchor.toml と lib.rs の declare_id を上記Program Idに書き換えてから、build:

% anchor build

anchor testを実行して、トランザクションのsignatureを取得:

% anchor test

tx => 2nDVg6C3bufg6Zw9McsnwipjEwZn9ahecj3wXzPR86gjg9Pr2EVbdTLxGGCDehRMkh1dFzKqCtR9oBQzYzZd5yGU

Solana Explorerでトランザクションを検索:
Solana Explorerでlocalnetのトランザクションを閲覧する方法

Client(JS)のデバッグ対策

(案1)anchor testでconsole.log

JSファイルに「console.log」を記述しておくだけで、「$ anchor test」を実行すると、ターミナルに情報が出力される。

basic-0.js

const anchor = require("@project-serum/anchor");

describe("basic-0", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.Provider.local());

  it("Uses the workspace to invoke the initialize instruction", async () => {
    // #region code
    // Read the deployed program from the workspace.
    const program = anchor.workspace.Basic0;
    console.log(program);

    // Execute the RPC.
    await program.rpc.initialize();
    // #endregion code
  });
});

ターミナルで実行

% anchor test
BPF SDK: /Users/user/.local/share/solana/install/releases/1.8.5/solana-release/bin/sdk/bpf
cargo-build-bpf child: rustup toolchain list -v
cargo-build-bpf child: cargo +bpf build --target bpfel-unknown-unknown --release
   Compiling basic-0 v0.1.0 (/Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/programs/basic-0)
    Finished release [optimized] target(s) in 1.13s
cargo-build-bpf child: /Users/user/.local/share/solana/install/releases/1.8.5/solana-release/bin/sdk/bpf/scripts/strip.sh /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/bpfel-unknown-unknown/release/basic_0.so /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0.so
cargo-build-bpf child: /Users/user/.local/share/solana/install/releases/1.8.5/solana-release/bin/sdk/bpf/dependencies/bpf-tools/llvm/bin/llvm-readelf --dyn-symbols /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0.so

To deploy this program:
  $ solana program deploy /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0.so
The program address will default to this keypair (override with --program-id):
  /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/basic-0/target/deploy/basic_0-keypair.json
yarn run v1.22.17
$ /Users/user/Desktop/blockchain/anchor-master/examples/tutorial/node_modules/.bin/mocha -t 1000000 tests/


  basic-0
Program {
  _idl: {
    version: '0.0.0',
    name: 'basic_0',
    instructions: [ [Object] ],
    metadata: { address: 'Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS' }
  },
  _provider: Provider {
    connection: Connection {
      _commitment: 'recent',
      _confirmTransactionInitialTimeout: undefined,
      _rpcEndpoint: 'http://localhost:8899',
      _rpcWsEndpoint: 'ws://localhost:8900/',
      _rpcClient: [ClientBrowser],
      _rpcRequest: [Function (anonymous)],
      _rpcBatchRequest: [Function (anonymous)],
      _rpcWebSocket: [Client],
      _rpcWebSocketConnected: false,
      _rpcWebSocketHeartbeat: null,
      _rpcWebSocketIdleTimeout: null,
      _disableBlockhashCaching: false,
      _pollingBlockhash: false,
      _blockhashInfo: [Object],
      _accountChangeSubscriptionCounter: 0,
      _accountChangeSubscriptions: {},
      _programAccountChangeSubscriptionCounter: 0,
      _programAccountChangeSubscriptions: {},
      _rootSubscriptionCounter: 0,
      _rootSubscriptions: {},
      _signatureSubscriptionCounter: 0,
      _signatureSubscriptions: {},
      _slotSubscriptionCounter: 0,
      _slotSubscriptions: {},
      _logsSubscriptionCounter: 0,
      _logsSubscriptions: {},
      _slotUpdateSubscriptionCounter: 0,
      _slotUpdateSubscriptions: {}
    },
    wallet: NodeWallet { payer: [Keypair] },
    opts: { preflightCommitment: 'recent', commitment: 'recent' }
  },
  _programId: PublicKey {
    _bn: <BN: da075cb2ff5ec6817613de530b692a8735477769da47430cbd8154335c4a8327>
  },
  _coder: Coder {
    instruction: InstructionCoder {
      idl: [Object],
      ixLayout: [Map],
      sighashLayouts: [Map]
    },
    accounts: AccountsCoder { accountLayouts: Map(0) {} },
    events: EventCoder { layouts: Map(0) {} }
  },
  _events: EventManager {
    _programId: PublicKey {
      _bn: <BN: da075cb2ff5ec6817613de530b692a8735477769da47430cbd8154335c4a8327>
    },
    _provider: Provider {
      connection: [Connection],
      wallet: [NodeWallet],
      opts: [Object]
    },
    _eventParser: EventParser { coder: [Coder], programId: [PublicKey] },
    _eventCallbacks: Map(0) {},
    _eventListeners: Map(0) {},
    _listenerIdCount: 0
  },
  rpc: { initialize: [AsyncFunction: rpc] },
  instruction: { initialize: [Function: ix] { accounts: [Function (anonymous)] } },
  transaction: { initialize: [Function: txFn] },
  account: {},
  simulate: { initialize: [AsyncFunction: simulate] },
  state: undefined
}
    ✔ Uses the workspace to invoke the initialize instruction (341ms)


  1 passing (344ms)

✨  Done in 5.55s.

参考

デバッグ

Solana Developing with Rust - Logging

msg! has two forms:

    msg!("A string");

or

    msg!(0_64, 1_64, 2_64, 3_64, 4_64);

Both forms output the results to the program logs. If a program so wishes they can emulate println! by using format!:

    msg!("Some variable: {:?}", variable);

言語別にデバッグ

「Rust x Anchor x TypeScript」の組み合わせになるため、もし、RustまたはTypeScriptだけのデバッグでよければ、個別にファイルを作成してデバッグもできる。

  • Rustだったら cargo new してデバッグ埋めてプログラムを書いて cargo run
  • TypeScriptだったら tsc init してデバッグ埋めてプログラムを書いて ts-node

たとえば、エラーは出ないのに「Rust x Anchor x TypeScript」で統合して動かすとエラーになり、デバッグもうまくできない、という場合は手間になるが、言語別に書いてデバッグするのがよさそう。