BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

op-succinct 代码分析- proposer

代码分析

1. 启动

proposer/op/proposer/service.go

func (ps *ProposerService) Start(_ context.Context) error {
    ps.Log.Info("Starting Proposer")
    return ps.driver.StartL2OutputSubmitting()
}

proposer/op/proposer/driver.go

func (l *L2OutputSubmitter) StartL2OutputSubmitting() error {
    ...
    // 当使用缓存数据库重新启动提议者时,我们需要将处于见证生成状态的所有证明标记为失败,然后重试。
    witnessGenReqs, err := l.db.GetAllProofsWithStatus(proofrequest.StatusWITNESSGEN)
    if err != nil {
        return fmt.Errorf("failed to get witness generation pending proofs: %w", err)
    }
    for _, req := range witnessGenReqs {
        err = l.RetryRequest(req, ProofStatusResponse{})
        if err != nil {
            return fmt.Errorf("failed to retry request: %w", err)
        }
    }

    // 验证合约的聚合和范围验证密钥的配置以及汇总配置哈希。
    err = l.ValidateConfig(l.Cfg.L2OutputOracleAddr.Hex()) // 向 l.Cfg.OPSuccinctServerUrl+"/validate_config" 配置的proposer/succinct/bin/server.rs 去请求数据
    ...
    go l.loop() // loop 负责创建和提交下一个输出

2. 循环检查和提交

proposer/op/proposer/driver.go

// loopL2OO 定期轮询 L2OO 以提出下一个区块,如果当前最终确定(或安全)的区块超过了下一个区块,则它会提出该区块。
func (l *L2OutputSubmitter) loopL2OO(ctx context.Context) {
    ticker := time.NewTicker(l.Cfg.PollInterval) // 检查间隔
    for {
        select {
        case <-ticker.C:
            // 获取提议者的当前指标。
            metrics, err := l.GetProposerMetrics(ctx)
            if err != nil {
                l.Log.Error("failed to get metrics", "err", err)
                continue
            }
            l.Log.Info("Proposer status", "metrics", metrics)

            // 1) 将准备好进行证明的范围证明放入队列中。根据最新的 L2 最终区块和当前的 L2 不安全头来确定这些范围证明。
            l.Log.Info("Stage 1: Getting Range Proof Boundaries...")
            err = l.GetRangeProofBoundaries(ctx)
            if err != nil {
                l.Log.Error("failed to get range proof boundaries", "err", err)
                continue
            }

            // 2) 检查 PROVING 请求的状态。如果成功返回,我们将验证其是否已保存在磁盘上,并将状态设置为“COMPLETE”。如果失败或超时,我们将状态设置为“FAILED”(如果是跨度证明,则将请求分成两半以重试)。
            l.Log.Info("Stage 2: Processing PROVING requests...")
            err = l.ProcessProvingRequests()
            if err != nil {
                l.Log.Error("failed to update PROVING requests", "err", err)
                continue
            }

            // 3) 检查 WITNESSGEN 请求的状态。如果见证生成请求处于 WITNESSGEN 状态的时间超过超时时间,则将状态设置为 FAILED 并重试。
            l.Log.Info("Stage 3: Processing WITNESSGEN requests...")
            err = l.ProcessWitnessgenRequests()
            if err != nil {
                l.Log.Error("failed to update WITNESSGEN requests", "err", err)
                continue
            }

            // 4) 确定从 L2OO 合约上的最新区块开始,是否存在连续的跨度证明链。如果有,则为所有跨度证明排队一个聚合证明。
            l.Log.Info("Stage 3: Deriving Agg Proofs...")
            err = l.DeriveAggProofs(ctx)
            if err != nil {
                l.Log.Error("failed to generate pending agg proofs", "err", err)
                continue
            }

            // 5) 从证明者网络请求所有未请求的证明。任何状态为“UNREQ”的数据库条目都表示它已排队并准备就绪。我们从证明者网络请求所有这些(span 和 agg)。对于 agg 证明,我们还会提前检查区块哈希。
            l.Log.Info("Stage 4: Requesting Queued Proofs...")
            err = l.RequestQueuedProofs(ctx)
            if err != nil {
                l.Log.Error("failed to request unrequested proofs", "err", err)
                continue
            }

            // 6) 在链上提交聚合证明。如果我们在数据库中有一个完整的聚合证明等待处理,我们会将其提交到链上。
            l.Log.Info("Stage 5: Submitting Agg Proofs...")
            err = l.SubmitAggProofs(ctx)
            if err != nil {
                l.Log.Error("failed to submit agg proofs", "err", err)
            }

2.1 GetRangeProofBoundaries

proposer/op/proposer/range.go

// 将准备好进行证明的范围证明放入队列中。根据最新的 L2 最终区块和当前的 L2 不安全头来确定这些范围证明。
func (l *L2OutputSubmitter) GetRangeProofBoundaries(ctx context.Context) error {
    // nextBlock 等于 DB 的 `EndBlock` 列中的最高值加 1。
    latestL2EndBlock, err := l.db.GetLatestEndBlock()
    if err != nil {
        if ent.IsNotFound(err) {
            latestEndBlockU256, err := l.l2ooContract.LatestBlockNumber(&bind.CallOpts{Context: ctx}) // 如果本地没有记录,则从合约读取
            if err != nil {
                return fmt.Errorf("failed to get latest output index: %w", err)
            } else {
                latestL2EndBlock = latestEndBlockU256.Uint64()
            }
        } else {
            l.Log.Error("failed to get latest end requested", "err", err)
            return err
        }
    }
    newL2StartBlock := latestL2EndBlock

    rollupClient, err := dial.DialRollupClientWithTimeout(ctx, dial.DefaultDialTimeout, l.Log, l.Cfg.RollupRpc) // 连接L2
    if err != nil {
        return err
    }

    // 获取最新的最终确定的 L2 区块。
    status, err := rollupClient.SyncStatus(ctx) // optimism_syncStatus
    if err != nil {
        l.Log.Error("proposer unable to get sync status", "err", err)
        return err
    }
    // 注意:最初,这使用的是 L1 最终区块。但为了满足新 API,我们现在使用 L2 最终区块。
    newL2EndBlock := status.FinalizedL2.Number

    spans := l.SplitRangeBasic(newL2StartBlock, newL2EndBlock)

    // 将每个跨度添加到数据库。如果没有跨度,我们将不会创建任何证明。
    for _, span := range spans {
        err := l.db.NewEntry(proofrequest.TypeSPAN, span.Start, span.End)
        l.Log.Info("New range proof request.", "start", span.Start, "end", span.End)
        if err != nil {
            l.Log.Error("failed to add span to db", "err", err)
            return err
        }
    }

    return nil
}
// CreateSpans 创建一个从开始到结束大小为 MaxBlockRangePerSpanProof 的跨度列表。注意:跨度 i 的结束 = 跨度 i+1 的开始。
func (l *L2OutputSubmitter) SplitRangeBasic(start, end uint64) []Span {
    spans := []Span{}
    // 从开始到结束创建大小为 MaxBlockRangePerSpanProof 的跨度。每个跨度都从前一个跨度结束的地方开始。继续,直到我们在到达终点之前无法再容纳另一个完整的跨度。
    for i := start; i+l.Cfg.MaxBlockRangePerSpanProof <= end; i += l.Cfg.MaxBlockRangePerSpanProof {
        spans = append(spans, Span{Start: i, End: i + l.Cfg.MaxBlockRangePerSpanProof})
    }
    return spans
}
总结

GetRangeProofBoundaries 根据初始高度(本地记录优先,否则查找合约记录)和当前L2最终区块高度,根据设置参数MaxBlockRangePerSpanProof拆分跨度区块,创建对应的Proof任务

2.2 ProcessProvingRequests

proposer/op/proposer/prove.go

// 检查 PROVING 请求的状态。如果成功返回,我们将验证其是否已保存在磁盘上,并将状态设置为“COMPLETE”。如果失败或超时,我们将状态设置为“FAILED”(如果是跨度证明,则将请求分成两半,以重试)。
func (l *L2OutputSubmitter) ProcessProvingRequests() error {
    reqs, err := l.db.GetAllProofsWithStatus(proofrequest.StatusPROVING) // 获取所有当前处于 PROVING 状态的证明请求
    if err != nil {
        return err
    }
    for _, req := range reqs {
        proofStatus, err := l.GetProofStatus(req.ProverRequestID)
        if err != nil {
            l.Log.Error("failed to get proof status for ID", "id", req.ProverRequestID, "err", err)
            l.Metr.RecordError("get_proof_status", 1) // 记录获取证明状态调用的错误。
            return err
        }
        if proofStatus.FulfillmentStatus == SP1FulfillmentStatusFulfilled {
            l.Log.Info("Fulfilled Proof", "id", req.ProverRequestID)
            err = l.db.AddFulfilledProof(req.ID, proofStatus.Proof) // 更新数据库中的证明并将状态更新为完成。
            if err != nil {
                l.Log.Error("failed to update completed proof status", "err", err)
                return err
            }
            continue
        }

        if proofStatus.FulfillmentStatus == SP1FulfillmentStatusUnfulfillable {
            // 记录失败原因。
            l.Log.Info("Proof is unfulfillable", "id", req.ProverRequestID)
            l.Metr.RecordProveFailure("unfulfillable")

            err = l.RetryRequest(req, proofStatus) // 如果为区间,则拆分成2个
            if err != nil {
                return fmt.Errorf("failed to retry request: %w", err)
            }
        }
    }

    return nil
}
func (l *L2OutputSubmitter) RetryRequest(req *ent.ProofRequest, status ProofStatusResponse) error {
    err := l.db.UpdateProofStatus(req.ID, proofrequest.StatusFAILED)
    if err != nil {
        l.Log.Error("failed to update proof status", "err", err)
        return err
    }

    // // 如果出现执行错误,且请求是 SPAN 证明,且区块范围 > 1,则将请求拆分为两个请求。这可能是由于 SP1 OOM 造成的,因为区块范围较大且交易较多。
    // TODO:一旦使用嵌入式分配器,就可以删除此解决方案,因为这样程序就永远不会出现 OOM。
    if req.Type == proofrequest.TypeSPAN && status.ExecutionStatus == SP1ExecutionStatusUnexecutable && req.EndBlock-req.StartBlock > 1 {
        // 将请求拆分为两个请求。
        midBlock := (req.StartBlock + req.EndBlock) / 2
        err = l.db.NewEntry(req.Type, req.StartBlock, midBlock)
        if err != nil {
            l.Log.Error("failed to retry first half of proof request", "err", err)
            return err
        }
        err = l.db.NewEntry(req.Type, midBlock+1, req.EndBlock)
        if err != nil {
            l.Log.Error("failed to retry second half of proof request", "err", err)
            return err
        }
    } else {
        // 重试同一请求。
        err = l.db.NewEntry(req.Type, req.StartBlock, req.EndBlock)
        if err != nil {
            l.Log.Error("failed to retry proof request", "err", err)
            return err
        }
    }

    return nil
}
总结

ProcessProvingRequests 根据本地db记录,依次向服务端l.Cfg.OPSuccinctServerUrl+"/status/"+proofId获取所有当前处于PROVING的任务最新状态,如果SP1已生成完成,则更新本地数据为完成,如果失败则进行重试(如果为区间,则拆分成2个)

2.3 ProcessWitnessgenRequests

proposer/op/proposer/prove.go

func (l *L2OutputSubmitter) ProcessWitnessgenRequests() error {
    // 获取当前处于 WITNESSGEN 状态的所有证明请求。
    reqs, err := l.db.GetAllProofsWithStatus(proofrequest.StatusWITNESSGEN)
    if err != nil {
        return err
    }
    for _, req := range reqs {
        // 如果请求处于 WITNESSGEN 状态的时间超过超时时间(20分钟),则将状态设置为 FAILED。
        if req.LastUpdatedTime+uint64(WITNESSGEN_TIMEOUT.Seconds()) < uint64(time.Now().Unix()) {
            l.RetryRequest(req, ProofStatusResponse{}) // 如果超时,重试请求
        }
    }

    return nil
}

2.4 DeriveAggProofs

proposer/op/proposer/prove.go

// 使用 L2OO 合约查找下一个证明必须覆盖的区块范围。检查数据库以查看我们是否有足够的跨度证明来请求覆盖此范围的聚合证明。如果是,则将聚合证明排队在数据库中以供稍后请求。
func (l *L2OutputSubmitter) DeriveAggProofs(ctx context.Context) error {
    latest, err := l.l2ooContract.LatestBlockNumber(&bind.CallOpts{Context: ctx})
    if err != nil {
        return fmt.Errorf("failed to get latest L2OO output: %w", err)
    }

    // 这将获取下一个块号,即 currentBlock + submissionInterval。
    minTo, err := l.l2ooContract.NextBlockNumber(&bind.CallOpts{Context: ctx})
    if err != nil {
        return fmt.Errorf("failed to get next L2OO output: %w", err)
    }

    created, end, err := l.db.TryCreateAggProofFromSpanProofs(latest.Uint64(), minTo.Uint64()) // 尝试从覆盖范围 [from, minTo) 的跨度证明中创建 AGG 证明。 如果创建了新的 AGG 证明,则返回 true,否则返回 false。
    if err != nil {
        return fmt.Errorf("failed to create agg proof from span proofs: %w", err)
    }
    if created {
        l.Log.Info("created new AGG proof", "from", latest.Uint64(), "to", end)
    }

    return nil
}

2.5 RequestQueuedProofs

// proposer/op/proposer/driver.go

// 从证明者网络请求所有未请求的证明。任何状态为“UNREQ”的数据库条目都表示它已排队并准备就绪。我们从证明者网络请求所有这些(span 和 agg)。对于 agg 证明,我们还会提前检查区块哈希。
func (l *L2OutputSubmitter) RequestQueuedProofs(ctx context.Context) error {
    nextProofToRequest, err := l.db.GetNextUnrequestedProof()
    if err != nil {
        return fmt.Errorf("failed to get unrequested proofs: %w", err)
    }
    if nextProofToRequest == nil {
        return nil
    }

    if nextProofToRequest.Type == proofrequest.TypeAGG {
        if nextProofToRequest.L1BlockHash == "" {
            blockNumber, blockHash, err := l.checkpointBlockHash(ctx) // 获取L1最新区块-1的head, 并把区块高度通过sendCheckpointTransaction方法发送交易,l.Cfg.L2OutputOracleAddr->checkpointBlockHash 写入合约
            if err != nil {
                l.Log.Error("failed to checkpoint block hash", "err", err)
                return err
            }
            nextProofToRequest, err = l.db.AddL1BlockInfoToAggRequest(nextProofToRequest.StartBlock, nextProofToRequest.EndBlock, blockNumber, blockHash.Hex()) // 创建新Proof任务
            if err != nil {
                l.Log.Error("failed to add L1 block info to AGG request", "err", err)
            }

            // 等待下一次循环,这样我们就有了添加了块信息的版本
            return nil
        } else {
            l.Log.Info("found agg proof with already checkpointed l1 block info")
        }
    } else {
        witnessGenProofs, err := l.db.GetNumberOfRequestsWithStatuses(proofrequest.StatusWITNESSGEN)
        if err != nil {
            return fmt.Errorf("failed to count witnessgen proofs: %w", err)
        }
        provingProofs, err := l.db.GetNumberOfRequestsWithStatuses(proofrequest.StatusPROVING)
        if err != nil {
            return fmt.Errorf("failed to count proving proofs: %w", err)
        }

        // 见证生成请求的数量上限为 MAX_CONCURRENT_WITNESS_GEN。这可以防止见证生成服务器产生的进程使机器过载。一旦 https://github.com/anton-rs/kona/issues/553 修复,我们可能就可以删除此检查。
        if witnessGenProofs >= MAX_CONCURRENT_WITNESS_GEN {
            l.Log.Info("max witness generation reached, waiting for next cycle")
            return nil
        }

        // 并发证明的总数上限为 MAX_CONCURRENT_PROOF_REQUESTS。
        if (witnessGenProofs + provingProofs) >= int(l.Cfg.MaxConcurrentProofRequests) {
            l.Log.Info("max concurrent proof requests reached, waiting for next cycle")
            return nil
        }
    }
    go func(p ent.ProofRequest) {
        l.Log.Info("requesting proof from server", "type", p.Type, "start", p.StartBlock, "end", p.EndBlock, "id", p.ID)
        // 将证明状态设置为 WITNESSGEN。
        err = l.db.UpdateProofStatus(p.ID, proofrequest.StatusWITNESSGEN)
        if err != nil {
            l.Log.Error("failed to update proof status", "err", err)
            return
        }

        // 根据模拟配置请求证明类型。
        err = l.RequestProof(p, l.Cfg.Mock)
        if err != nil {
            // 如果证明请求失败,我们应该将其添加到队列中以待重试。
            err = l.RetryRequest(nextProofToRequest, ProofStatusResponse{})
            if err != nil {
                l.Log.Error("failed to retry request", "err", err)
            }

        }
    }(*nextProofToRequest)

    return nil
}
// RequestProof 处理模拟和真实证明请求
func (l *L2OutputSubmitter) RequestProof(p ent.ProofRequest, isMock bool) error {
    jsonBody, err := l.prepareProofRequest(p)
    if err != nil {
        return err
    }

    if isMock { // 开启Mock
        proofData, err := l.requestMockProof(p.Type, jsonBody)
        if err != nil {
            return fmt.Errorf("mock proof request failed: %w", err)
        }

        // 对于模拟证明,一旦生成了“模拟证明”,就将状态设置为 PROVING。AddFulfilledProof 期望证明处于 PROVING 状态。
        err = l.db.UpdateProofStatus(p.ID, proofrequest.StatusPROVING)
        if err != nil {
            return fmt.Errorf("failed to set proof status to proving: %w", err)
        }
        return l.db.AddFulfilledProof(p.ID, proofData)
    }

    // 向见证生成服务器请求真实证明。从网络返回证明 ID。
    proofID, err := l.requestRealProof(p.Type, jsonBody)
    if err != nil {
        return fmt.Errorf("real proof request failed: %w", err)
    }

    // 检索到证明者 ID 后,将证明状态设置为 PROVING。只有状态为 PROVING、SUCCESS 或 FAILED 的证明才有证明者请求 ID。
    err = l.db.UpdateProofStatus(p.ID, proofrequest.StatusPROVING)
    if err != nil {
        return fmt.Errorf("failed to set proof status to proving: %w", err)
    }

    return l.db.SetProverRequestID(p.ID, proofID)
}

2.6 SubmitAggProofs

// 在链上提交聚合证明。如果我们在数据库中有一个完整的聚合证明等待处理,我们会将其提交到链上。
func (l *L2OutputSubmitter) SubmitAggProofs(ctx context.Context) error {
    // 从 L2OutputOracle 合约获取最新的输出索引
    latestBlockNumber, err := l.l2ooContract.LatestBlockNumber(&bind.CallOpts{Context: ctx})
    if err != nil {
        return fmt.Errorf("failed to get latest output index: %w", err)
    }

    // 从下一个索引开始检查已完成的 AGG 证明
    completedAggProofs, err := l.db.GetAllCompletedAggProofs(latestBlockNumber.Uint64())
    if err != nil {
        return fmt.Errorf("failed to query for completed AGG proof: %w", err)
    }

    if len(completedAggProofs) == 0 {
        return nil
    }

    // 选择具有最高 L2 块编号的聚合证明。
    sort.Slice(completedAggProofs, func(i, j int) bool {
        return completedAggProofs[i].EndBlock > completedAggProofs[j].EndBlock
    })

    // 提交具有最高 L2 块编号的聚合证明。
    aggProof := completedAggProofs[0]
    output, err := l.FetchOutput(ctx, aggProof.EndBlock) // 通过optimism_outputAtBlock 健全性检查,例如在出现不良 RPC 缓存的情况下
    if err != nil {
        return fmt.Errorf("failed to fetch output at block %d: %w", aggProof.EndBlock, err)
    }
    err = l.proposeOutput(ctx, output, aggProof.Proof, aggProof.L1BlockNumber)// 通过proposeOutput 进行Proof的提交
    if err != nil {
        return fmt.Errorf("failed to propose output: %w", err)
    }

    return nil
}

Solana - JS/TS 客户端

Anchor 提供了一个 Typescript 客户端库(@coral-xyz/anchor),简化了使用 JavaScript 或 TypeScript 从客户端与 Solana 程序交互的过程。

客户端程序

Program 要使用客户端库,首先使用Anchor 生成的IDL 文件创建一个实例 。
创建 的实例Program需要程序的 IDL 和 AnchorProvider。AnchorProvider是结合了两件事的抽象:

  • Connection- 与Solana 集群的连接 (即 localhost、devnet、mainnet)
  • Wallet- (可选)用于支付和签署交易的默认钱包

前端/节点

当使用钱包适配器与前端集成时 ,您需要设置AnchorProvider和Program。

import { Program, AnchorProvider, setProvider } from "@coral-xyz/anchor";
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
import type { HelloAnchor } from "./idlType";
import idl from "./idl.json";

const { connection } = useConnection();
const wallet = useAnchorWallet();

const provider = new AnchorProvider(connection, wallet, {});
setProvider(provider);

export const program = new Program(idl as HelloAnchor, {
  connection,
});

在上面的代码片段中:

  • idl.json是 Anchor 生成的 IDL 文件,可以 /target/idl/<program-name>.json在 Anchor 项目中找到。
  • idlType.ts是 IDL 类型(用于 TS), /target/types/<program-name>.ts在 Anchor 项目中找到。

Program或者,您可以仅使用 IDL 和Solana 集群创建 的实例Connection。这意味着没有默认的 Wallet,但允许您使用Program来获取帐户或构建指令,而无需连接钱包

import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
import { Program } from "@coral-xyz/anchor";
import type { HelloAnchor } from "./idlType";
import idl from "./idl.json";

const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

export const program = new Program(idl as HelloAnchor, {
  connection,
});

测试文件

Anchor 会自动在新项目的默认测试文件中设置一个Program实例。但是,此设置与在 Anchor 工作区外部初始化的方式不同Program ,例如在 React 或 Node.js 应用程序中。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";

describe("hello_anchor", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
  });
});

调用指令

一旦Program使用程序 IDL 设置,您就可以使用 Anchor MethodsBuilder 来:

  • 建立个别指令
  • 建立交易
  • 建立并发送交易

基本格式如下:

await program.methods // 这是用于从程序的 IDL 创建指令调用的构建器 API
  .instructionName(instructionData) // 接下来.methods,从程序 IDL 中指定指令的名称,并将任何所需的参数作为逗号分隔的值传递。
  .accounts({}) // 按照IDL中指定的方式传入指令所需的账户地址
  .signers([]) //  可选地传入指令所需的作为附加签名者的密钥对数组。这通常在创建新帐户时使用,其中帐户地址是新生成的密钥对的公钥。请注意,.signers只有在使用.rpc()时才应使用。当使用 .transaction()或 时.instruction(),应在发送之前将签名者添加到交易中。
  .rpc();

Anchor 提供了多种构建程序指令的方法:

.rpc

该 rpc() 方法 发送 带有指定指令的签名交易TransactionSignature并返回。
当使用 时.rpc,Wallet来自 的Provider将自动包含在签名者中。

// Generate keypair for the new account
const newAccountKp = new Keypair();

const data = new BN(42);
const transactionSignature = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([newAccountKp])
  .rpc();

.transaction()

该 transaction() 方法 使用指定的指令构建Transaction 而不发送交易。

// Generate keypair for the new account
const newAccountKp = new Keypair();

const data = new BN(42);
const transaction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .transaction();

const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

.instruction()

该 instruction() 方法 使用指定的指令构建TransactionInstruction 。如果您想手动将指令添加到交易并将其与其他指令相结合,这很有用。

// Generate keypair for the new account
const newAccountKp = new Keypair();

const data = new BN(42);
const instruction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .instruction();

const transaction = new Transaction().add(instruction);

const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

获取账户

客户端Program简化了获取和反序列化由 Anchor 程序创建的帐户的过程。
使用program.account后跟 IDL 中定义的帐户类型的名称。Anchor 提供了多种方法来获取帐户。

.all()

用于 all() 获取特定帐户类型的所有现有帐户。

const accounts = await program.account.newAccount.all();

memcmp

使用memcmp(内存比较)来筛选与特定偏移量处的特定值匹配的帐户数据。使用memcmp需要您了解要获取的帐户类型的数据字段的字节布局。
计算偏移量时,请记住 Anchor 程序创建的账户中的前 8 个字节是为账户鉴别器保留的。

const accounts = await program.account.newAccount.all([
  {
    memcmp: {
      offset: 8,
      bytes: "",
    },
  },
]);

fetch()

用于 fetch() 获取单个账户的账户数据

const account = await program.account.newAccount.fetch(ACCOUNT_ADDRESS);

fetchMultiple()

fetchMultiple() 通过传入账户地址数组来获取多个账户的账户数据

const accounts = await program.account.newAccount.fetchMultiple([
  ACCOUNT_ADDRESS_ONE,
  ACCOUNT_ADDRESS_TWO,
]);

原文:https://solana.com/docs/programs/anchor/client-typescript

Solana - IDL 文件

接口定义语言 (IDL) 文件提供了描述程序指令和账户的标准化 JSON 文件。此文件简化了链上程序与客户端应用程序集成的过程。

IDL 的主要优势:

  • 标准化:提供一致的格式来描述程序的指令和帐户
  • 客户端生成:用于生成与程序交互的客户端代码

anchor build命令生成一个位于 的 IDL 文件 /target/idl/<program-name>.json
下面的代码片段突出显示了程序、IDL 和客户端如何相互关联。

程序指令

IDL 中的数组instructions直接对应于程序中定义的指令。它指定每条指令所需的帐户和参数。

  • Program:下面的程序包含一条initialize指令,指定其所需的帐户和参数。
  • 程序帐户:accounts IDL 中的数组对应于程序中用宏注释的结构体。#[account]这些结构体定义了程序创建的账户中存储的数据。
use anchor_lang::prelude::*;

declare_id!("BYFW1vhC1ohxwRbYoLbAWs86STa25i9sD5uEusVjTYNd");

#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct NewAccount {
    data: u64,
}

IDL

生成的 IDL 文件包含标准化 JSON 格式的指令,包括其名称、帐户、参数和鉴别器。

{
  "address": "BYFW1vhC1ohxwRbYoLbAWs86STa25i9sD5uEusVjTYNd",
  "metadata": {
    "name": "hello_anchor",
    "version": "0.1.0",
    "spec": "0.1.0",
    "description": "Created with Anchor"
  },
  "instructions": [
    {
      "name": "initialize",
      "discriminator": [175, 175, 109, 31, 13, 152, 155, 237],
      "accounts": [
        {
          "name": "new_account",
          "writable": true,
          "signer": true
        },
        {
          "name": "signer",
          "writable": true,
          "signer": true
        },
        {
          "name": "system_program",
          "address": "11111111111111111111111111111111"
        }
      ],
      "args": [
        {
          "name": "data",
          "type": "u64"
        }
      ]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "discriminator": [176, 95, 4, 118, 91, 177, 125, 232]
    }
  ],
  "types": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [
          {
            "name": "data",
            "type": "u64"
          }
        ]
      }
    }
  ]
}

Client

然后使用IDL文件生成与程序交互的客户端,简化调用程序指令的过程。

import * as anchor from "@coral-xyz/anchor";
import { Program, BN } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";
import { Keypair } from "@solana/web3.js";
import assert from "assert";

describe("hello_anchor", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const wallet = provider.wallet as anchor.Wallet;
  const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

  it("initialize", async () => {
    // Generate keypair for the new account
    const newAccountKp = new Keypair();

    // Send transaction
    const data = new BN(42);
    const transactionSignature = await program.methods
      .initialize(data)
      .accounts({
        newAccount: newAccountKp.publicKey,
        signer: wallet.publicKey,
      })
      .signers([newAccountKp])
      .rpc();

    // Fetch the created account
    const newAccount = await program.account.newAccount.fetch(
      newAccountKp.publicKey,
    );

    console.log("Transaction signature: ", transactionSignature);
    console.log("On-chain data is:", newAccount.data.toString());
    assert(data.eq(newAccount.data));
  });
});

鉴别器

Anchor 为程序中的每个指令和账户类型分配一个唯一的 8 字节鉴别符。这些鉴别符作为标识符来区分不同的指令或账户类型。
鉴别器是使用前缀的 Sha256 哈希的前 8 个字节与指令或帐户名称相结合生成的。从 Anchor v0.30 开始,这些鉴别器包含在 IDL 文件中。
请注意,使用 Anchor 时,您通常不需要直接与这些鉴别器交互。本节主要介绍如何生成和使用鉴别器。

instructions

指令鉴别器被程序用来决定在调用时要执行哪条具体指令。
当调用 Anchor 程序指令时,鉴别符将作为指令数据的前 8 个字节包含在内。此操作由 Anchor 客户端自动完成。

IDL

  "instructions": [
    {
      "name": "initialize",
      "discriminator": [175, 175, 109, 31, 13, 152, 155, 237],
       ...
    }
  ]

accounts

账户鉴别器用于在反序列化链上数据时识别具体的账户类型,在账户创建时设置。

IDL

  "accounts": [
    {
      "name": "NewAccount",
      "discriminator": [176, 95, 4, 118, 91, 177, 125, 232]
    }
  ]

指令的鉴别器是前缀的 Sha256 哈希的前 8 个字节global加上指令名称。

例如:

sha256("global:initialize")

十六进制输出:

af af 6d 1f 0d 98 9b ed d4 6a 95 07 32 81 ad c2 1b b5 e0 e1 d7 73 b2 fb bd 7a b5 04 cd d4 aa 30

前 8 个字节用作指令的鉴别符。

af = 175
af = 175
6d = 109
1f = 31
0d = 13
98 = 152
9b = 155
ed = 237

您可以在此处的Anchor 代码库中找到鉴别器生成的实现 ,该代码库在此处使用 。

总结:https://solana.com/docs/programs/anchor/idl

Solana- Anchor项目文件结构

以下是 Anchor 工作区中默认文件结构的概述:

.
├── .anchor
│   └── program-logs
├── app
├── migrations
├── programs
│   └── [project-name]
│       └── src
│           ├── lib.rs
│           ├── Cargo.toml
│           └── Xargo.toml
├── target
│   ├── deploy
│   │   └── [project-name]-keypair.json
│   ├── idl
│   │   └── [project-name].json
│   └── types
│       └── [project-name].ts
├── tests
│   └── [project-name].ts
├── Anchor.toml
├── Cargo.toml
└── package.json

程序文件夹

/programs文件夹包含项目的 Anchor 程序。单个工作区可以包含多个程序。

测试文件夹

/tests文件夹包含项目的测试文件。创建项目时会为您创建一个默认测试文件。

目标文件夹

/target文件夹包含构建输出。主要子文件夹包括:

  • /deploy:包含您的程序的密钥对和程序二进制文件。
  • /idl:包含程序的 JSON IDL。
  • /types:包含 IDL 的 TypeScript 类型。

Anchor.toml文件

该Anchor.toml文件为您的项目配置工作区设置。

.anchor 文件夹

包含一个program-logs文件,其中包含上次运行测试文件的事务日志。

应用程序文件夹

该/app文件夹是一个空文件夹,您可以选择将其用于前端代码。

Solana Hello World(安装和故障排除)

这是 Solana 的 Hello World 教程。我们将引导您完成安装 Solana 的步骤并解决可能出现的问题。
如果您遇到问题,请查看本文末尾的故障排除部分。

安装步骤

安装 Rust

如果您已经安装了 Rust,请跳过此步骤。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装 Yarn

您需要它来运行单元测试。如果您已经安装了 yarn,请跳过此步骤。

corepack enable

安装 Solana cli

我们强烈建议使用stable版本,而不是latest。 Solana 安装不再支持符号通道名称(edge、beta、stable),因此我们必须指定版本。

sh -c "$(curl -sSfL https://release.anza.xyz/v2.1.5/install)"

安装锚点

Anchor 是 Solana 开发的框架。它在很多方面与 hardhat 非常相似。

cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

avm install latest
avm use latest

测试安装

初始化并构建一个锚点程序(用于 hello world)

调用您的程序day_1而不是day1因为 Anchor 有时似乎会在 idl路径上插入下划线。

anchor init day_1
cd day_1
anchor build

根据您的机器和互联网连接,此步骤可能需要一段时间。这也是您可能遇到安装问题的地方,因此如有必要,请参阅故障排除部分。

配置 Solana 在本地主机上运行

solana config set --url localhost

运行测试验证器节点

在新的 shell 中(而不是在 Anchor 项目中)运行以下命令。但不要关闭运行的 shell anchor build。这会在您的机器上运行本地(测试)Solana 节点实例:

solana-test-validator

确保 program_id 与 Anchor 键同步

返回 Anchor 项目的 shell 并运行以下命令:

anchor keys sync

运行测试

在 Anchor 项目中运行此命令

anchor test --skip-local-validator

上面的命令运行我们程序的测试。如果您尚未创建测试钱包,Anchor 将为您提供如何创建钱包的说明。我们在此不提供这些说明,因为它取决于您的操作系统和文件结构。您可能还需要通过在终端中运行来为自己空投一些本地 Sol 。您可以通过在命令行中solana airdrop 100 {YOUR_WALLET_ADDRESS}运行来获取您的钱包地址。solana address

预期输出如下:

你好世界

现在让我们让程序输出“Hello, world!”。将以下标有 的行添加NEW LINE HERE到programs/day_1/src/lib.rs。

use anchor_lang::prelude::*;

declare_id!("...");

#[program]
pub mod day_1 {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Hello, world!"); // **** NEW LINE HERE ****
        Ok(())
    }
}

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

再次运行测试之前,请终止本地验证器进程并使用以下命令重新启动它:

solana-test-validator --reset

再次运行测试

anchor test --skip-local-validator

通过运行查找日志文件

ls .anchor/program-logs/

打开该文件即可看到记录的“Hello world”

实时 Solana 日志

或者,您可以通过打开第三个 shell 并运行以下命令来查看日志:

solana logs

现在再次运行测试,您应该在运行的终端中看到相同的消息solana logs。

program 查询

查询已安装

solana program show --programs

关闭程序

solana program close 3ynNB373...2iTyg --bypass-warning

请注意,一旦关闭某个程序,则不能重新使用该程序 ID 来部署新程序。

问答

  1. 为什么 declared_id! 和 msg! 后面有感叹号?
    在 Rust 中,感叹号表示这些是宏。我们将在后面的教程中重新讨论宏。
  2. 我需要一个初始化函数吗?
    不,这是由 Anchor 框架自动生成的。您可以随意命名。
    在此上下文中,initialize 这个名称没有什么特殊之处,因此我们可以将其更改为任何我们喜欢的名称。这与其他一些关键字和语言不同,例如 main 在某些语言中是一个特殊名称,或者在 Solidity 中,constructor 是一个特殊名称。
    练习:尝试将initialize中的programs/day_1/src/lib.rs和initialize中的重命名为 ,tests/day_1.ts然后initialize2再次运行测试。请参见下面橙色圆圈中标记的更改。
  3. 为什么我们要使用 –skip-local-validator 来运行测试?
    当测试针对某个节点运行时,我们将能够查询该节点的状态变化。如果您无法让节点运行,则可以anchor test在不使用--skip-local-validator标志的情况下运行。但是,这将使您的开发和测试变得更加困难,因此我们建议让本地验证器正常工作。

故障排除

Solana 是一款快速发展的软件,您可能会遇到安装问题。我们在以下部分记录了您最有可能遇到的问题。
我们的教程系列使用以下版本编写: Anchor = 版本 0.29.0 Solana = 版本 1.16.25 * Rustc = 1.77.0-nightly
您可以通过运行以下命令来更改 Anchor 版本:

avm install 0.29.0
avm use 0.29.0
  1. 错误:solana-program v1.18.0无法构建包
    error: package `solana-program v1.18.0` cannot be built because it requires rustc 1.72.0 or newer, while the currently active rustc version is 1.68.0-dev
    Either upgrade to rustc 1.72.0 or newer, or use
    cargo update -p solana-program@1.18.0 --precise ver

    检查您正在使用的 Solana 版本solana --version。然后将该版本插入到ver上面的内容中。示例解决方案如下所示:

  2. 错误[E0658]:使用不稳定库功能“build_hasher_simple_hash_one”
    如果出现以下错误:
    error[E0658]: use of unstable library feature 'build_hasher_simple_hash_one'
    --> src/random_state.rs:463:5
    |
    463 | / fn hash_one<T: Hash>(&self, x: T) -> u64 {
    464 | | RandomState::hash_one(self, x)
    465 | | }
    | |_____^
    |
    = note: see issue #86161 https://github.com/rust-lang/rust/issues/86161 for more information
    = help: add #![feature(build_hasher_simple_hash_one)] to the crate attributes to enable

    运行以下命令:cargo update -p ahash@0.8.7 --precise 0.8.6
    来源:https ://solana.stackexchange.com/questions/8800/cant-build-hello-world

  3. 错误:部署程序失败:错误处理指令 1:自定义程序错误:0x1
    Error: Deploying program failed: Error processing Instruction 1: custom program error: 0x1
    There was a problem deploying: Output { status: ExitStatus(unix_wait_status(256)), stdout: "", stderr: "" }.

    如果出现此错误,则表示您的密钥未同步。运行anchor keys sync

  4. 错误:无法发送交易:交易模拟失败:尝试加载不存在的程序
    您的密钥未同步。运行anchor keys sync
  5. 错误:您配置的 rpc 端口:8899 已被使用
    当验证器在后台运行时,您anchor test没有运行--skip-local-validator。请关闭验证器并运行,anchor test或在验证器运行时运行anchor test --skip-local-validator。跳过本地验证器意味着跳过它为项目创建的临时验证器,而不是在后台运行的验证器。
  6. 错误:帐户 J7t…zjK 资金不足,无法消费
    运行以下命令将 100 SOL 空投到您的开发地址:
    solana airdrop 100 J7t...zjK
  7. 错误:RPC 请求错误:集群版本查询失败
    Error: RPC request error: cluster version query failed: error sending request for url (http://localhost:8899/): error trying to connect: tcp connect error: Connection refused (os error 61)
    There was a problem deploying: Output { status: ExitStatus(unix_wait_status(256)), stdout: "", stderr: "" }.

    这意味着solana-test-validator不在后台运行。solana-test-validator在另一个 shell 中运行。

  8. 线程“main”因“调用Option::unwrap()某个None值”而惊慌失措
    thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', /Users/username/.cargo/git/checkouts/anchor-50c4b9c8b5e0501f/347c225/lang/syn/src/idl/file.rs:214:73
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

    你可能还没跑anchor build

  9. 我在使用 Mac,出现错误:无法启动验证器:无法在测试分类账中创建分类账:块存储错误
    按照此Stack Exchange 线程中的说明进行操作。
  10. 我的 Mac 上有 node.js,但是没有 corepack
    运行以下命令:
    brew install corepack
    brew link --overwrite corepack

    来源:https ://stackoverflow.com/questions/70082424/command-not-found-corepack-when-installing-yarn-on-node-v17-0-1

  11. 错误:不是目录
    BPF SDK: /Users/rareskills/.local/share/solana/install/releases/stable-43daa37937907c10099e30af10a5a0b43e2dd2fe/solana-release/bin/sdk/bpf
    cargo-build-bpf child: rustup toolchain list -v
    cargo-build-bpf child: rustup toolchain link bpf /Users/rareskills/.local/share/solana/install/releases/stable-43daa37937907c10099e30af10a5a0b43e2dd2fe/solana-release/bin/sdk/bpf/dependencies/bpf-tools/rust
    error: not a directory:

    清除缓存:运行rm -rf ~/.cache/solana/*

  12. 错误:target/idl/day_1.json 不存在。您运行了吗anchor build?
    创建一个新项目并将其命名为 day_1 而不是 day1。Anchor 似乎会在某些机器上默默插入下划线。

原文:https://www.rareskills.io/post/hello-world-solana