op-succinct 代码分析- proposer succinct server
代码分析 proposer/succinct/bin/server.rs let app = Router::new() .route("/request_span_proof", post(request_span_proof)) // 请求对一系列区块的证明。 .route("/request_agg_proof", post(request_agg_proof)) // 请求一组子证明的聚合证明。 .route("/request_mock_span_proof", post(request_mock_span_proof)) // 请求对一系列区块的Mock证明。 .route("/request_mock_agg_proof", post(request_mock_agg_proof))// 请求一组子证明的Mock聚合证明。 .route("/status/:proof_id", get(get_proof_status)) // 获取证明的状态。 .route("/validate_config", post(validate_config)) // 验证 L2 输出 Oracle 的配置 .layer(DefaultBodyLimit::disable()) .layer(RequestBodyLimitLayer::new(102400 * 1024 * 1024)) .with_state(global_hashes); let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string()); let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) .await .unwrap(); info!("Server listening on {}", listener.local_addr().unwrap()); request_span_proof async fn request_span_proof( State(state): State<ContractConfig>, Json(payload): Json<SpanProofRequest>, ) -> Result<(StatusCode, Json<ProofResponse>), AppError> { info!("Received span proof request: {:?}", payload); let fetcher = match OPSuccinctDataFetcher::new_with_rollup_config(RunContext::Docker).await { // get_rpcs() 通过环境变量 L1_RPC L1_BEACON_RPC L2_RPC L2_NODE_RPC 获取相应配置，并通过fetch_and_save_rollup_config 获取L2的rollup_config Ok(f) => f, Err(e) => { error!("Failed to create data fetcher: {}", e); return Err(AppError(e)); } }; let host_cli = match fetcher .get_host_cli_args( // 获取给定块号的 L2 输出数据，并将启动信息保存到数据目录中带有 block_number 的文件中。返回要传递给 datagen 的本机主机的参数。 payload.start, payload.end, ProgramType::Multi, CacheMode::DeleteCache, ) .await { Ok(cli) => cli, Err(e) => { error!("Failed to get host CLI args: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to get host CLI args: {}", e ))); } }; // 启动服务器和本机客户端并设置超时时间。 // 注意：理想情况下，服务器应调用执行本机的单独进程 // 主机，并返回客户端可以轮询以检查证明是否已提交的 ID。 let mut witnessgen_executor = WitnessGenExecutor::new(WITNESSGEN_TIMEOUT, RunContext::Docker); if let Err(e) = witnessgen_executor.spawn_witnessgen(&host_cli).await { // 为给定的主机 CLI 生成见证生成进程，并将其添加到正在进行的进程列表中。 error!("Failed to spawn witness generation: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to spawn witness generation: {}", e ))); } // 记录运行见证生成过程时出现的任何错误。 if let Err(e) = witnessgen_executor.flush().await { error!("Failed to generate witness: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to generate witness: {}", e ))); } let sp1_stdin = match get_proof_stdin(&host_cli) { // 获取标准输入来为给定的 L2 声明生成证明。 Ok(stdin) => stdin, Err(e) => { error!("Failed to get proof stdin: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to get proof stdin: {}", e ))); } }; let private_key = match env::var("SP1_PRIVATE_KEY") { Ok(private_key) => private_key, Err(e) => { error!("Failed to get SP1 private key: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to get SP1 private key: {}", e ))); } }; let rpc_url = match env::var("PROVER_NETWORK_RPC") { Ok(rpc_url) => rpc_url, Err(e) => { error!("Failed to get PROVER_NETWORK_RPC: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to get PROVER_NETWORK_RPC: {}", e ))); } }; let mut prover = NetworkProverV2::new(&private_key, Some(rpc_url.to_string()), false); // 根据private key和rpc 创建prover // 使用预留策略路由到特定集群。 prover.with_strategy(FulfillmentStrategy::Reserved); // 由于范围证明很大，因此将模拟设置为 false。 env::set_var("SKIP_SIMULATION", "true"); let vk_hash = match prover.register_program(&state.range_vk, RANGE_ELF).await { // 注册elf/range-elf Ok(vk_hash) => vk_hash, Err(e) => { error!("Failed to register program: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to register program: {}", e ))); } }; let proof_id = match prover .request_proof( // 向证明者网络请求证明，并返回请求 ID。 &vk_hash, &sp1_stdin, ProofMode::Compressed, // 压缩证明模式。还支持 Core，Plonk，Groth16 1_000_000_000_000, None, ) .await { Ok(proof_id) => proof_id, Err(e) => { error!("Failed to request proof: {}", e); return Err(AppError(anyhow::anyhow!("Failed to request proof: {}", e))); } }; env::set_var("SKIP_SIMULATION", "false"); Ok((StatusCode::OK, Json(ProofResponse { proof_id }))) } pub async fn request_proof( &self, vk_hash: &[u8], stdin: &SP1Stdin, mode: ProofMode, cycle_limit: u64, timeout: Option<Duration>, ) -> Result<Vec<u8>> { // Get the timeout. let timeout_secs = timeout.map(|dur| dur.as_secs()).unwrap_or(TIMEOUT_SECS); log::info!("Requesting proof with cycle limit: {}", cycle_limit); // Request the proof with retries. let response = with_retry( || async { self.client .request_proof( vk_hash, stdin, mode, SP1_CIRCUIT_VERSION, self.strategy, timeout_secs, cycle_limit, ) .await }, timeout, "requesting proof", ) .await?; // 记录请求 ID 和交易哈希。 let tx_hash_hex = "0x".to_string() + &hex::encode(response.tx_hash); let request_id = response.body.unwrap().request_id; let request_id_hex = "0x".to_string() + &hex::encode(request_id.clone()); log::info!("Created request {} in transaction {}", request_id_hex, tx_hash_hex); if self.client.rpc_url() == DEFAULT_PROVER_NETWORK_RPC { // "https://rpc.production.succinct.tools/" log::info!("View in explorer: https://network.succinct.xyz/request/{}", request_id_hex); } Ok(request_id) } // 使用给定的验证密钥哈希和标准输入创建证明请求。 pub async fn request_proof( &self, vk_hash: &[u8], stdin: &SP1Stdin, mode: ProofMode, version: &str, strategy: FulfillmentStrategy, timeout_secs: u64, cycle_limit: u64, ) -> Result<RequestProofResponse> { // 计算截止期限。 let start = SystemTime::now(); let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Invalid start time"); let deadline = since_the_epoch.as_secs() + timeout_secs; // 创建 stdin artifact. let mut store = self.get_store().await?; let stdin_uri = self.create_artifact_with_content(&mut store, &stdin).await?; // 发送请求 let mut rpc = self.get_rpc().await?; // 获取 ProverNetwork rpc let nonce = self.get_nonce().await?; // 从ProverNetwork中获取nonce let request_body = RequestProofRequestBody { // 组装请求体 nonce, version: format!("sp1-{}", version), vk_hash: vk_hash.to_vec(), mode: mode.into(), strategy: strategy.into(), stdin_uri, deadline, cycle_limit, }; let request_response = rpc .request_proof(RequestProofRequest { // 调用的为sp1客户端服务 /network.ProverNetwork/RequestProof format: MessageFormat::Binary.into(), signature: request_body.sign(&self.signer).into(), // 使用私钥进行签名 body: Some(request_body), }) .await? .into_inner(); Ok(request_response) } https://github.com/succinctlabs/sp1/blob/bfb0c6d8e045b5f40422b9c06cb0e9ee21b3c19c/crates/sdk/src/network/proto/network.rs#L3 // This file is @generated by prost-build. #[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, ::prost::Message)] pub struct RequestProofRequest { /// The message format of the body. #[prost(enumeration = "MessageFormat", tag = "1")] pub format: i32, /// The signature of the sender. #[prost(bytes = "vec", tag = "2")] pub signature: ::prost::alloc::vec::Vec<u8>, /// The body of the request. #[prost(message, optional, tag = "3")] pub body: ::core::option::Option<RequestProofRequestBody>, } 具体SP1服务相关逻辑，后续文章单独分析 request_agg_proof // 请求一组子证明的聚合证明。 async fn request_agg_proof( State(state): State<ContractConfig>, Json(payload): Json<AggProofRequest>, ) -> Result<(StatusCode, Json<ProofResponse>), AppError> { info!("Received agg proof request"); let mut proofs_with_pv: Vec<SP1ProofWithPublicValues> = payload .subproofs .iter() .map(|sp| bincode::deserialize(sp).unwrap()) .collect(); let boot_infos: Vec<BootInfoStruct> = proofs_with_pv .iter_mut() .map(|proof| proof.public_values.read()) .collect(); let proofs: Vec<SP1Proof> = proofs_with_pv // 从payload中获取多个证明 .iter_mut() .map(|proof| proof.proof.clone()) .collect(); let l1_head_bytes = hex::decode( payload .head .strip_prefix("0x") .expect("Invalid L1 head, no 0x prefix."), )?; let l1_head: [u8; 32] = l1_head_bytes.try_into().unwrap(); let fetcher = match OPSuccinctDataFetcher::new_with_rollup_config(RunContext::Docker).await { Ok(f) => f, Err(e) => return Err(AppError(anyhow::anyhow!("Failed to create fetcher: {}", e))), }; let headers = match fetcher .get_header_preimages(&boot_infos, l1_head.into()) // 获取与启动信息对应的标头的原映像。具体来说，获取与启动信息和最新的 L1 头对应的标头。 通过get_earliest_l1_head_in_batch从boot_infos中获取最早的L1 Head 作为start, get_l1_header 获取最新的 L1 Head（在链上验证）的完整标头, 通过fetch_headers_in_range获取start到end多个区块的headers .await { Ok(h) => h, Err(e) => { error!("Failed to get header preimages: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to get header preimages: {}", e ))); } }; let private_key = env::var("SP1_PRIVATE_KEY")?; let rpc_url = env::var("PROVER_NETWORK_RPC")?; let mut prover = NetworkProverV2::new(&private_key, Some(rpc_url.to_string()), false); // 使用预留策略路由到特定集群。 prover.with_strategy(FulfillmentStrategy::Reserved); let stdin = match get_agg_proof_stdin(proofs, boot_infos, headers, &state.range_vk, l1_head.into()) { // 获取聚合证明的标准输入。 Ok(s) => s, Err(e) => { error!("Failed to get agg proof stdin: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to get agg proof stdin: {}", e ))); } }; let vk_hash = match prover.register_program(&state.agg_vk, AGG_ELF).await { // 注册elf/aggregation-elf Ok(vk_hash) => vk_hash, Err(e) => { error!("Failed to register program: {}", e); return Err(AppError(anyhow::anyhow!( "Failed to register program: {}", e ))); } }; let proof_id = match prover .request_proof( // 使用给定的验证密钥哈希和标准输入创建证明请求。具体看上面的request_span_proof->request_proof &vk_hash, &stdin, ProofMode::Groth16, // 和request_span_proof有区别，ProofMode::Compressed, 1_000_000_000_000, None, ) .await { Ok(id) => id, Err(e) => { error!("Failed to request proof: {}", e); return Err(AppError(anyhow::anyhow!("Failed to request proof: {}", e))); } }; Ok((StatusCode::OK, Json(ProofResponse { proof_id }))) } pub fn get_agg_proof_stdin( proofs: Vec<SP1Proof>, boot_infos: Vec<BootInfoStruct>, headers: Vec , multi_block_vkey: &sp1_sdk::SP1VerifyingKey, latest_checkpoint_head: B256, ) -> Result<SP1Stdin> { let mut stdin = SP1Stdin::new(); for proof in proofs { let SP1Proof::Compressed(compressed_proof) = proof else { panic!(); }; stdin.write_proof(*compressed_proof, multi_block_vkey.vk.clone()); } // 将聚合输入写入标准输入。 stdin.write(&AggregationInputs { boot_infos, latest_l1_checkpoint_head: latest_checkpoint_head, multi_block_vkey: multi_block_vkey.hash_u32(), }); // Head在使用 bincode 序列化时存在问题，因此请改用 serde_json。 let headers_bytes = serde_cbor::to_vec(&headers).unwrap(); stdin.write_vec(headers_bytes); Ok(stdin) } 总结 request_span_proof：通过get_proof_stdin获取证明所需参数，使用ProofMode::Compressed压缩证明模式，通过request_proof向sp1服务申请生成proof request_agg_proof: 将获取到的多个proof,使用ProofMode::Groth16证明模式，通过request_proof向sp1服务申请生成聚合proof
代码分析 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 }
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
接口定义语言 (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
以下是 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 的步骤并解决可能出现的问题。 如果您遇到问题，请查看本文末尾的故障排除部分。 安装步骤 安装 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 来部署新程序。 问答 为什么 declared_id! 和 msg! 后面有感叹号？ 在 Rust 中，感叹号表示这些是宏。我们将在后面的教程中重新讨论宏。 我需要一个初始化函数吗？ 不，这是由 Anchor 框架自动生成的。您可以随意命名。 在此上下文中，initialize 这个名称没有什么特殊之处，因此我们可以将其更改为任何我们喜欢的名称。这与其他一些关键字和语言不同，例如 main 在某些语言中是一个特殊名称，或者在 Solidity 中，constructor 是一个特殊名称。 练习：尝试将initialize中的programs/day_1/src/lib.rs和initialize中的重命名为 ，tests/day_1.ts然后initialize2再次运行测试。请参见下面橙色圆圈中标记的更改。 为什么我们要使用 –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 错误：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上面的内容中。示例解决方案如下所示： 错误[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 错误：部署程序失败：错误处理指令 1：自定义程序错误：0x1Error: 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。 错误：无法发送交易：交易模拟失败：尝试加载不存在的程序 您的密钥未同步。运行anchor keys sync。 错误：您配置的 rpc 端口：8899 已被使用 当验证器在后台运行时，您anchor test没有运行--skip-local-validator。请关闭验证器并运行，anchor test或在验证器运行时运行anchor test --skip-local-validator。跳过本地验证器意味着跳过它为项目创建的临时验证器，而不是在后台运行的验证器。 错误：帐户 J7t…zjK 资金不足，无法消费 运行以下命令将 100 SOL 空投到您的开发地址：solana airdrop 100 J7t...zjK 错误：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 中运行。 线程“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 我在使用 Mac，出现错误：无法启动验证器：无法在测试分类账中创建分类账：块存储错误 按照此Stack Exchange 线程中的说明进行操作。 我的 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 错误：不是目录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/*。 错误：target/idl/day_1.json 不存在。您运行了吗anchor build？ 创建一个新项目并将其命名为 day_1 而不是 day1。Anchor 似乎会在某些机器上默默插入下划线。 原文：https://www.rareskills.io/post/hello-world-solana
Solana 编程语言 Solana 的主要编程语言是Rust，但也支持C、C++甚至Python 。 Solana 编码语言以类似的方式使用 Rust、C 和 C++。我们将在后面的部分讨论 Python。 Rust 是一门编译型语言，如果你在电脑上编译 Rust，它最终会变成 LLVM-IR（低级虚拟机中间表示），LLVM 会把它变成可以在你的机器（x86、arm64 等）上运行的字节码。 在 Solana 中，该序列如下所示： 1）将 Rust 编译为 LLVM-IR，然后编译为 BPF（伯克利数据包过滤器），并将字节码存储在区块链上。 2）验证器 JIT 编译（即时编译）BPF 为与其硬件兼容的指令集，通常是 x86，但 arm64 可能是另一个常见目标。 你可能会问，数据包过滤器？Solana 编程语言为什么要与互联网数据包有关？ 这实际上是 Solana 的一个聪明的设计选择。 用户空间与内核空间 Linux 有内核空间和用户空间的概念。如果你想要执行诸如打开文件或启动另一个进程之类的操作，你的可执行文件需要请求操作系统替它执行这些操作。假设你编写了一个 Python 脚本来打开一个文件并打印出每个偶数行。文件字节码的实际加载发生在内核空间中，但是一旦将字节码提供给脚本，对 ASCII 的解释以及确定行号是偶数还是奇数就会发生在用户空间中。 这种抽象存在的原因有很多，但其中一个明显的原因就是安全性。并非每个用户或可执行文件都应该能够打开或执行任意文件。操作系统决定允许使用哪些“API”。（顺便说一句，打开文件的“API”在操作系统术语中技术上称为“系统调用”）。 同样，程序和可执行文件不应被允许任意访问传入的互联网数据包。默认情况下，它们必须进行系统调用，以请求操作系统允许查看只能从内核空间访问的数据包。 这里必须强调一个重要概念：在用户空间和内核空间之间来回转换通常很慢。 如果您正在过滤传入的互联网数据包，那么用户空间和内核空间之间就会有很多来回跳转。想象一下将每个传入数据包从内核空间复制到用户空间。这将产生大量开销。 这就是 BPF 被发明的原因。你可以在内核空间内运行可执行文件来避免这种跳跃。 但是，如果您对拥有内核特权有所了解，您就会知道这非常危险！如果出现错误，控制内核（操作系统）可能会导致计算机崩溃。更糟糕的是，如果执行了恶意代码，则损害是无限的。 当然，BPF 设计者已经想到了这一点。在执行 BPF 代码之前，会对其进行验证，以确保其运行一段固定的时间，即必须终止、只能访问指定的内存区域，并遵循其他适当的限制。 顺便说一句，自从 BPF 发明以来，它的用途就已不仅限于过滤数据包，但它的名字却一直沿用至今。 为什么 Solana 语言使用 BPF 通过利用现有的确保 BPF 程序安全的研究，Solana 可以在运行速度最快的地方运行智能合约 — 内核内部！仔细想想，这真是太了不起了。您可以在操作系统最敏感（但最高效）的部分 — 操作系统内核中运行任何人都可以编写的不受信任的智能合约。Solana 可以利用数十年来在该领域的研究和投资来获得良好的性能提升。 BPF 不是机器指令；它仍然是自己的一组字节码。但是，它可以被 JIT 编译为各种 CPU 架构。 Solana 链上程序通过LLVM 编译器基础结构编译 为 可执行和可链接格式 (ELF)，其中包含伯克利数据包过滤器 (BPF)字节码 的变体 。 由于 Solana 使用 LLVM 编译器基础架构，因此可以使用任何能够针对 LLVM 的 BPF 后端的编程语言编写程序。 BPF 提供了一套高效的 指令集，可以在解释型虚拟机中执行，也可以作为高效的即时编译的本机指令执行。 回到 Rust、C 和 C++ 这三种编程语言早已得到 LLVM 的支持。这是 Solana 可以利用数十年投资的另一个领域（是的，用几十年来谈论技术有点奇怪，但LLVM 早在 2003 年就问世了）。 您可以使用 C 或 C++ 作为 Solana 编程语言，但工具支持会少得多。通常建议您使用 Rust，即使您是 C 或 C++ 专家。如果您已经精通 C++，那么 Rust 将很容易学习。 你需要了解多少 Rust 才能编写 Solana 程序？ 不是很多，但仍然需要一些学习。Rust 不是一种可以“谷歌一下”的语言。例如，假设您从 Java 或 Scala 背景开始使用 Ruby 编程。在这种情况下，您可以很容易地向 Google 询问等效的编程模式（您的代码可能看起来不太符合惯用语，但它可读且功能齐全）。如果（当）您从 Stackoverflow 复制和粘贴代码，您仍然会对代码的作用有很好的直觉。 但是，如果你使用 Rust 来做这件事，你会遇到一些令人沮丧的障碍。Rust 的语法很难查找（试着在搜索引擎中查找“#”），并且它有一些其他编程语言中没有的概念。 Rust 是一种广泛的语言。但是，您只需要了解它的一个子集。我们的Solana 开发课程会同时教授 Rust 和 Solana，这样您就可以只关注 Rust 中您需要的部分。 Solana 如何使用 Python 将 Rust、C 或 C++ 编译为 BPF 非常简单。对于 Python，情况则截然不同；显然，Python 是一种解释型语言，而不是一种可以用 LLVM 编译的语言。 简单来说，Python 被转换成 Rust，然后一切都按上面的方式运行。 如果你想要确切的工作流程，可以在seahorse-lang github上查看详细的文档 Solana 程序通常不是用原始 Rust 编写的；大多数开发人员使用 Anchor Framework。因此，尽管 Seahorse 执行相当典型的转译，但它也利用了计划中的框架相似性。 Seahorse 框架与 Anchor 框架紧密建模，因此 Python 代码可以转换为 Rust 代码，而 Rust 代码的编写方式与 Anchor 框架中的编写方式紧密建模。 请注意，该项目目前处于测试阶段。 Solana 使用 solidity 吗？ 是的，可以用 Solidity 编写 Solana 应用程序，但有些实验性。solang solidity 编译器是为了支持将 Solidity 编译为 BPF 而构建的。 Solana 客户端编程语言 Solana 客户端本身（即在为区块链提供支持的节点上运行的程序，而不是程序（智能合约））是用 Rust 编写的。目前，Jump Crypto 正在重新实现 Solana 客户端firedancer，该客户端完全用 C 语言编写。 附录：Rust 正在将 BPF 迁移到 SBF。 自 2022 年 10 月起，Solana 开始从 BPF 迁移到 SBF（Solana 二进制格式）。截至撰写本文时，有关此更改的文档非常稀少，但这不会影响大多数开发人员。如果您的构建工具配置为编译为 BPF，您将收到弃用警告，但一切仍将运行。只需更改您的构建标志即可。 资源 https://www.kernel.org/doc/html/latest/bpf/instruction-set.html https://docs.rs/solana_rbpf/latest/solana_rbpf/ebpf/index.html https://www.youtube。 com/watch?v=5jQvuPWpzcE https://ebpf.io/what-is-ebpf/ https://www.youtube.com/watch?v=Q8eY67hDvkc 原文：https://www.rareskills.io/post/solana-smart-contract-language
了解以太坊和 Solana 智能合约之间的区别。 您可以在 Solana 区块链上编写和部署程序。 程序（在其他协议中也称为智能合约）是从 DeFi 和 NFT 到社交媒体和游戏等链上活动的基础。 程序处理从用户或其他程序收到的指令。 所有程序都不维护状态。换句话说，程序使用的数据通过指令发送到单独的账户。 该程序本身存储在一个executable已检查的帐户中。 所有程序均归 BPF Loader 所有，并由 Solana Runtime 执行。 开发人员通常使用 Rust 或 C++ 编写程序。不过，任何针对 LLVM 和 BPF 后端的语言都可以选择。 所有程序都有一个单独的入口点，在此进行指令处理（即process_instruction）。始终包含以下参数： program_id: pubkey, accounts: array, instruction_data: byte array 与大多数其他区块链不同，Solana 将数据和代码完全分离。程序与之交互的所有数据都存储在单独的账户中，并通过指令进行调用。 这种模式允许综合单一程序通过各种帐户运行，而无需额外部署。这种模式的一个常见示例可以在 Native 和 SPL 程序之间看到。 本机程序和 Solana 程序库 (SPL) Solana 有许多程序可作为链上交互的核心构建块。 这些程序分为本机程序和 Solana 程序库 (SPL) 程序。 原生程序提供运行验证器所需的基础功能。其中最著名的程序之一是System Program，负责管理新账户并在两个组之间转移 SOL。 SPL 程序支持各种链上活动，包括代币创建、兑换、借贷、权益池生成和链上名称服务。SPL 代币程序可以直接通过 CLI 调用。其他程序（如关联代币帐户程序）通常配置为自定义程序 编写程序 程序通常用 Rust 和 C++ 开发。但是，您可以使用任何针对 LLVM 和 BPF 后端的语言进行开发。Neon Labs 和 Solang 最近的努力使 EVM 兼容性成为可能，允许开发人员用 Solidity 编写程序。 大多数基于 Rust 的程序遵循以下架构： 文件 描述 lib.rs 注册模块 entrypoint.rs 程序的入口点 instruction.rs 程序 API，指令数据（反）序列化 processor.rs 程序逻辑 state.rs 程序对象，（反）序列化状态 error.rs 程序特定错误 最近，Anchor 已成为一种程序开发框架。Anchor 减少了样板代码并简化了（反）序列化处理，类似于 Ruby on Rails，但适用于基于 Rust 的程序。程序通常在 Localhost 和 Devnet 环境中开发和测试，然后部署到 Testnet 和 Mainnet。 Solana 支持以下环境： 集群环境 RPC 连接 URL 主网测试版 https://api.mainnet-beta.solana.com 测试网 https://api.testnet.solana.com 开发网 https://api.devtnet.solana.com 本地主机 默认端口：8899（例如，http://localhost:8899） 一旦部署到环境，客户端就可以通过与每个集群的 RPC 连接与链上程序进行交互。 部署程序 开发人员可以通过 CLI 部署程序，如下所示： solana program deploy <PROGRAM_FILEPATH> 程序部署后，会被编译为 ELP 共享对象（包括 BPF 字节码）并上传到 Solana 集群。除非程序被标记为可执行并分配给 BPF 加载器，否则它们都存在于帐户内。帐户的地址用作 program_id在所有交易中引用程序的地址。 Solana 支持多个 BPF 加载器，包括最近的可升级 BPF 加载器。BPF 加载器负责管理程序帐户，并通过客户端的 program_id 实现这一点。 所有程序都有一个单独的入口点，在此进行指令处理（即 process_instruction）。参数始终包括： program_id: pubkey accounts: array instruction_data: byte array 一旦调用，程序就会由 Solana Runtime 执行。 概括 Solana 中的程序对应于 Solana 区块链上的“可执行账户”。 以太坊中的智能合约概念与 Solana 中的程序概念一致。 Solana 允许任何人发行代币，而无需部署额外的合约。 原文：https://solana.com/developers/evm-to-svm/smart-contracts
了解在以太坊和 Solana 上构建账户时的不同之处。 作为区块链网络，以太坊和 Solana 拥有独特的数据结构，充当全球公共计算机，在其网络上存储和共享数据。在本章中，我们旨在探索这些链如何构建其数据集。 以太坊中的“账户” 在以太坊中，“账户”是指拥有以太币并可以发送交易的实体。它包括存款和取款所需的地址，并分为以下几类： EOA（外部拥有账户）：外部拥有的账户，拥有私钥。可以将其视为个人钱包的账户。 CA（合约账户）：合约账户，持有智能合约代码。 以太坊中 EOA 与 CA 的一个关键区别是，EOA 不是智能合约，通常没有自己的存储。因此，EOA 的代码哈希被设置为“空”哈希，表示该帐户没有存储空间。 外部拥有账户 (EOA) 是具有私钥的账户，拥有私钥意味着控制对资金或合约的访问。私钥意味着控制对资金或合约的访问。EOA 中包含以下数据： 场地 描述 address 账户地址 balance 地址拥有的 ETH 数量（以 wei 为单位）。 nonce 计数器显示从账户发送的交易数量，以确保交易只处理一次。对于智能合约，它反映了账户创建的合约数量。 code hash EVM 上的账户代码。 storage hash (storage root) 对账户存储内容进行编码的 Merkle Patricia 树根节点的 256 位哈希值。此树对此账户存储内容的哈希值进行编码，默认情况下为空。 合约账户包含智能合约代码，而 EOA 根本无法拥有这些代码。此外，合约账户没有私钥。相反，它由智能合约代码的逻辑控制。此智能合约代码是在创建合约账户时记录在以太坊区块链上的，是由 EVM 执行的软件程序。 与 EOA 一样，合约账户有一个地址，可以发送和接收 Ether。但是，当交易的目的地是合约账户地址时，交易和交易数据将用作在 EVM 中执行合约的输入。除了 Ether，交易还可以包含指示要执行的合约特定功能的数据以及要传递给该功能的参数。因此，交易可以调用合约内的函数。如果 EOA 请求，合约还可以调用其他合约。但是，由于合约账户没有私钥，因此它无法签署交易，也无法自行发起交易。关系总结如下： EOA → EOA（确定） EOA → CA（确定） EOA → CA → CA（确定） CA → CA（不可能） Solana 中的“账户” Solana 中的帐户概念比以太坊中的要广泛一些。在 Solana 中，所有数据都是基于帐户存储和执行的。这意味着，在交易之间需要存储状态的每种情况下，都会使用帐户来保存状态。与 Linux 等操作系统中的文件类似，帐户可以存储在程序生命周期之外仍然存在的任意数据。此外，与文件一样，帐户包含元数据，可告知运行时谁可以访问数据以及如何访问。 在 Solana 的 Sealevel VM 中，所有账户都能够存储数据。那么，智能合约开发人员可以将数据存储在哪里呢？他们可以将数据存储在可执行账户拥有的非可执行账户 (PDA) 中。开发人员可以通过分配与其可执行账户地址相同的所有者来创建新账户来存储数据。 场地 描述 lamports 此帐户拥有的 lampor 数量。相当于 balance。 owner 此帐户的程序所有者。 executable 该账户是否可以处理指令。 data 该账户存储的原始数据字节数组。 rent_epoch 此帐户下次欠租金的时期。 然而，Solana 网络上存储数据的“账户”需要支付费用。这些账户包含有关其所含数据寿命的元数据，以名为“Lamports”的原生代币表示。账户存储在验证者的内存中，并支付“租金”以保留在那里。验证者定期扫描所有账户并收取租金。Lamports 降至零的账户将被自动删除，因为它们无法支付租金。如果一个账户包含足够数量的 Lamport，它将免收租金，并且不会单独扣除租金。 Solana 的账户分为以下两种类型，与以太坊类似： 可执行账户（程序账户）：这些是存储代码的智能合约，通常更简单地称为“程序”。 不可执行账户（数据账户）：这些账户可以接收代币或数据，但不能执行代码，因为可执行变量设置为“false”。 （*与以太坊不同，Solana 使用术语“程序”而不是“合约”。） 比较每个链中的帐户结构，可以发现以下差异。 以太坊账户 Solana 的账户 Account owner balance lamports nonce [无同等效力] code hash executable && data storage hash data [无同等效力] rent_epoch 那么，EOA 和 CA 与 Solana 的 Account 结构如何对应呢？ 它们可以进行如下映射。 EOA（外部拥有账户、钱包） → 不可执行的数据账户 然而，Solana 上的个人钱包由一系列 数据账户组成，这是一个比EOA 更广泛的概念。 CA（合约账户） → 可执行文件、程序帐户 虽然具有相同的概念，但以太坊的CA不能自行执行交易；它们必须由EOA执行。 账户抽象 以太坊一直在探索账户抽象的概念。以太坊中有两种类型的账户：EOA 和 CA，每种账户的功能截然不同。值得注意的是，合约账户 (CA) 无法生成或签署交易，这带来了很大的限制。交易必须通过 EOA 发起和签署，这意味着使用 21,000 gas 的基本费用，并增加了账户管理的复杂性。账户抽象旨在消除这些限制，允许单个账户同时执行 EOA 和合约账户的功能。 因此，可以对图表进行以下调整： EOA → EOA（确定） EOA → CA（确定） EOA → CA → CA（确定） EOA + CA (AA) → CA（现在，好的！） 例如，多重签名钱包或智能合约钱包需要在单独的 EOA 中存储少量以太坊以支付交易费用，这带来了需要随时间补充的不便。账户抽象允许单个账户执行合约并发出交易，从而改善了这种不便。Vitalik 通过 ERC-4337 向社区提出了这一概念，并于 2021 年被采纳，现已在以太坊网络中实现。 总而言之，账户抽象具有以下好处： 其他人支付我的交易费用，或者我为其他人支付。 使用 ERC-20 代币支付费用 设置自定义安全规则。 密钥丢失时的帐户恢复。 在受信任的设备或个人之间共享帐户安全。 批量交易（例如，一次性授权和执行掉期）。 为 dapp 和钱包开发人员提供更多机会来创新用户体验。 Solana 中实现了账户抽象吗？ Solana 自推出以来就实现了账户抽象（AA）。如前所述，Solana 将所有数据存储在称为“账户”的单元中，分为可执行（程序账户）和不可执行（数据账户）。从一开始，Solana 就支持程序创建和管理特定账户（即直接发起交易）的能力。此功能扩展了 Solana 中的账户抽象功能，称为程序派生地址（PDA）。与数据账户不同，Solana 程序是包含可执行代码的可执行账户。通过 PDA，开发人员可以设置交易签名的规则和机制，允许代表 Solana 网络认可和批准的受控账户（PDA）自主授权各种链上操作。因此，与以太坊不同，Solana 允许直接控制基于 Solana 程序的另一个程序，而无需繁琐的分层。 概括 Solana 的账户概念构建了链上的所有数据，所有数据都基于账户。 Solana 原生支持 AA，实现程序间自调用。 原文：https://solana.com/developers/evm-to-svm/accounts
了解以太坊和 Solana 在达成共识方面的差异。 众所周知，以太坊和 Solana 的共识机制均采用 PoS（权益证明）。它们都通过基于权益的验证器生成区块。尽管它们从根本上采用相同的共识，但以太坊目前记录的 TPS（每秒交易数）约为 30，而 Solana 则拥有 4000 TPS。这种差异间接表明了共识如何显著影响区块生成速度。本章将深入探讨这两条链之间的差异。 以太坊最初的共识 以太坊最初采用的是工作量证明（PoW）方式，比特币至今仍在使用这种方式。在 2022 年 8 月合并升级后，以太坊从工作量证明过渡到权益证明，现在采用上述的 PoS。 以太坊目前的共识 以太坊的核心是 PoS，但它特别采用了一种称为 Gasper 的共识算法。Gasper 是 Casper 友好最终性小工具 (Casper-FFG) 和 LMD-GHOST 分叉选择算法的组合。在深入研究 Gasper 之前，需要注意的是，以太坊最初是根据基于 PoW 的 Nakamoto 共识生成区块的。Nakamoto 共识遵循最长链规则，在分叉期间选择区块链中最长的链。因此，以太坊并没有完全放弃 PoW；它保留了 PoW 下的基本区块生成，同时在其上集成了 PoS 元素。 Casper (Casper-FFG) 是 Gasper 的一部分，它将某些区块升级为“最终确定”状态，确保网络参与者与常规链同步。它最初是在 Merge 升级期间为从基于 PoW 的链到 PoS 的过渡阶段提出的，现在部分贡献于更大的 Gasper 算法。LMD-GHOST（最新消息驱动最贪婪最重观察子树）是一种分叉选择算法，用于在各种分叉中选择最有效和最值得信赖的链。如果分叉生成多个新区块，验证者将通过证明消息进行投票，以确定将哪个区块附加到现有链。这些证明遵循从创世区块到最新区块（叶区块）的路径，选择由最新证明支持的区块。 Solana 的共识 Solana 中与以太坊各个共识算法对应的技术组件是什么？答案就在支持 PoS 的 Tower BFT 中。 首先，让我们了解一下 BFT 和 PBFT。BFT 是一种在分布式系统中实现节点间可靠共识的方法，源自“拜占庭将军问题”。即使某些节点是恶意的或不可靠的，它也能确保系统正常运行。PBFT 是 BFT 的一种实际实现，它通过确保所有节点之间对所有交易达成一致来保证系统的最终性和一致性。 Tower BFT 采用了 PBFT 的变体，但有一个根本区别：历史证明 (PoH) 在达成共识之前充当全局时钟。在 Solana 的实现中，PoH 用作网络时钟，用于安排区块、交易和数据的顺序。 以太坊经常会低效地更新其整个网络状态以用于特定交易，并且交易可能无法按顺序处理。相比之下，Solana 利用 PoH（历史证明）来支持 PoS。要确定两个事件之间的加密时间，需要一系列计算步骤。例如，考虑两张照片：一张是苹果，另一张是正在拍摄的照片。我们可以推断苹果的照片是先拍摄的。Solana 通过在数据中添加时间戳来跟踪它们的顺序，确保其结构不会混淆。 Tower BFT 使用 PoH，通过时间戳验证有效地确定区块、交易和数据的顺序。因此，验证者可以最终决定分叉，选择最受投票信任的链。以太坊缺乏像 PoH 那样的时间戳概念来进行整体状态同步，迫使验证者根据之前的哈希值进行计算以选择分叉。相反，基于 PoH 的 Solana Tower BFT 可以轻松达成共识，而无需进行完整的区块验证。 因此，比较可以总结如下： 以太坊 Solana Proof of Work » Proof of Stake Proof of Stake Casper 友好最终性小工具（Casper-FFG） Tower BFT + PoH LMD-GHOST 分叉选择规则 Tower BFT 概括 以太坊共识：基于 PoS 的 Gasper = Casper 友好最终性小工具（Casper-FFG）+ LMD-GHOST 分叉选择规则 Solana 的共识：基于 PoS 的 Tower BFT + PoH 原文：https://solana.com/developers/evm-to-svm/consensus