add output_smt_size but encording still is base64
This commit is contained in:
parent
9d0a21ba5c
commit
93244ecf62
108
log4rs_gbt.yml
108
log4rs_gbt.yml
|
@ -1,108 +0,0 @@
|
||||||
# GBT Client 日志配置文件
|
|
||||||
# 基于 log4rs 配置,用于GBT客户端的日志管理
|
|
||||||
refresh_rate: 30 seconds
|
|
||||||
|
|
||||||
appenders:
|
|
||||||
# 控制台输出
|
|
||||||
stdout:
|
|
||||||
kind: console
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%H:%M)} {h({l}):5} {m}{n}"
|
|
||||||
filters:
|
|
||||||
- kind: threshold
|
|
||||||
level: info
|
|
||||||
|
|
||||||
# GBT客户端日志文件
|
|
||||||
gbt:
|
|
||||||
kind: rolling_file
|
|
||||||
path: "{{log_dir}}/log/gbt/gbt.log"
|
|
||||||
policy:
|
|
||||||
kind: compound
|
|
||||||
trigger:
|
|
||||||
kind: size
|
|
||||||
limit: 10mb
|
|
||||||
roller:
|
|
||||||
kind: fixed_window
|
|
||||||
base: 1
|
|
||||||
count: 5
|
|
||||||
pattern: "{{log_dir}}/log/gbt/gbt.{}.log"
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}"
|
|
||||||
|
|
||||||
# ZMQ通信日志
|
|
||||||
zmq:
|
|
||||||
kind: rolling_file
|
|
||||||
path: "{{log_dir}}/log/gbt/zmq.log"
|
|
||||||
policy:
|
|
||||||
kind: compound
|
|
||||||
trigger:
|
|
||||||
kind: size
|
|
||||||
limit: 10mb
|
|
||||||
roller:
|
|
||||||
kind: fixed_window
|
|
||||||
base: 1
|
|
||||||
count: 5
|
|
||||||
pattern: "{{log_dir}}/log/gbt/zmq.{}.log"
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}"
|
|
||||||
|
|
||||||
# gRPC通信日志
|
|
||||||
grpc:
|
|
||||||
kind: rolling_file
|
|
||||||
path: "{{log_dir}}/log/gbt/grpc.log"
|
|
||||||
policy:
|
|
||||||
kind: compound
|
|
||||||
trigger:
|
|
||||||
kind: size
|
|
||||||
limit: 10mb
|
|
||||||
roller:
|
|
||||||
kind: fixed_window
|
|
||||||
base: 1
|
|
||||||
count: 5
|
|
||||||
pattern: "{{log_dir}}/log/gbt/grpc.{}.log"
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}"
|
|
||||||
|
|
||||||
# 根日志配置
|
|
||||||
root:
|
|
||||||
level: warn
|
|
||||||
appenders:
|
|
||||||
- stdout
|
|
||||||
|
|
||||||
loggers:
|
|
||||||
# GBT客户端日志
|
|
||||||
gbt:
|
|
||||||
level: debug
|
|
||||||
appenders:
|
|
||||||
- gbt
|
|
||||||
- stdout
|
|
||||||
additive: false
|
|
||||||
|
|
||||||
# ZMQ相关日志
|
|
||||||
zmq:
|
|
||||||
level: debug
|
|
||||||
appenders:
|
|
||||||
- zmq
|
|
||||||
- stdout
|
|
||||||
additive: false
|
|
||||||
|
|
||||||
# gRPC相关日志
|
|
||||||
tonic:
|
|
||||||
level: debug
|
|
||||||
appenders:
|
|
||||||
- grpc
|
|
||||||
- stdout
|
|
||||||
additive: false
|
|
||||||
|
|
||||||
# 其他第三方库日志
|
|
||||||
tokio_util:
|
|
||||||
level: warn
|
|
||||||
appenders:
|
|
||||||
- gbt
|
|
||||||
additive: false
|
|
||||||
|
|
||||||
h2:
|
|
||||||
level: warn
|
|
||||||
appenders:
|
|
||||||
- grpc
|
|
||||||
additive: false
|
|
290
src/main.rs
290
src/main.rs
|
@ -56,196 +56,14 @@ use tari_core::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tari_utilities::hex::Hex;
|
use tari_utilities::hex::Hex;
|
||||||
use hex::FromHex;
|
use tari_utilities::ByteArray;
|
||||||
|
use jmt::{JellyfishMerkleTree, KeyHash};
|
||||||
|
use jmt::mock::MockTreeStore;
|
||||||
|
use tari_core::chain_storage::SmtHasher;
|
||||||
|
use tari_core::blocks::Block as CoreBlock;
|
||||||
|
|
||||||
const LOG_TARGET: &str = "gbt::main";
|
const LOG_TARGET: &str = "gbt::main";
|
||||||
|
|
||||||
// 自定义的Block结构体,用于16进制序列化
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct HexBlock {
|
|
||||||
header: Option<HexBlockHeader>,
|
|
||||||
body: Option<serde_json::Value>, // 保持原有的body结构
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct HexBlockHeader {
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
hash: Vec<u8>,
|
|
||||||
version: u32,
|
|
||||||
height: u64,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
prev_hash: Vec<u8>,
|
|
||||||
timestamp: u64,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
output_mr: Vec<u8>,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
block_output_mr: Vec<u8>,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
kernel_mr: Vec<u8>,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
input_mr: Vec<u8>,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
total_kernel_offset: Vec<u8>,
|
|
||||||
nonce: u64,
|
|
||||||
pow: HexProofOfWork,
|
|
||||||
kernel_mmr_size: u64,
|
|
||||||
output_mmr_size: u64,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
total_script_offset: Vec<u8>,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
validator_node_mr: Vec<u8>,
|
|
||||||
validator_node_size: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct HexProofOfWork {
|
|
||||||
pow_algo: u64,
|
|
||||||
#[serde(serialize_with = "serialize_bytes_as_hex")]
|
|
||||||
pow_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义序列化函数:将字节数组序列化为16进制字符串
|
|
||||||
fn serialize_bytes_as_hex<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
let hex_string = hex::encode(bytes);
|
|
||||||
serializer.serialize_str(&hex_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将Block转换为HexBlock
|
|
||||||
fn convert_block_to_hex(block: &Block) -> Result<HexBlock> {
|
|
||||||
let header = if let Some(ref h) = block.header {
|
|
||||||
Some(HexBlockHeader {
|
|
||||||
hash: h.hash.clone(),
|
|
||||||
version: h.version,
|
|
||||||
height: h.height,
|
|
||||||
prev_hash: h.prev_hash.clone(),
|
|
||||||
timestamp: h.timestamp,
|
|
||||||
output_mr: h.output_mr.clone(),
|
|
||||||
block_output_mr: h.block_output_mr.clone(),
|
|
||||||
kernel_mr: h.kernel_mr.clone(),
|
|
||||||
input_mr: h.input_mr.clone(),
|
|
||||||
total_kernel_offset: h.total_kernel_offset.clone(),
|
|
||||||
nonce: h.nonce,
|
|
||||||
pow: HexProofOfWork {
|
|
||||||
pow_algo: h.pow.as_ref().map(|p| p.pow_algo).unwrap_or(0),
|
|
||||||
pow_data: h.pow.as_ref().map(|p| p.pow_data.clone()).unwrap_or_default(),
|
|
||||||
},
|
|
||||||
kernel_mmr_size: h.kernel_mmr_size,
|
|
||||||
output_mmr_size: h.output_mmr_size,
|
|
||||||
total_script_offset: h.total_script_offset.clone(),
|
|
||||||
validator_node_mr: h.validator_node_mr.clone(),
|
|
||||||
validator_node_size: h.validator_node_size,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将body转换为JSON值,保持原有结构
|
|
||||||
let body_json = serde_json::to_value(&block.body).map_err(|e| anyhow!("Body serialization error: {}", e))?;
|
|
||||||
|
|
||||||
Ok(HexBlock {
|
|
||||||
header,
|
|
||||||
body: Some(body_json),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义反序列化函数:将16进制字符串反序列化为字节数组
|
|
||||||
fn deserialize_hex_to_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let hex_string = String::deserialize(deserializer)?;
|
|
||||||
hex::FromHex::from_hex(&hex_string).map_err(|e: hex::FromHexError| serde::de::Error::custom(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用于反序列化的结构体
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct HexBlockDeserialize {
|
|
||||||
header: Option<HexBlockHeaderDeserialize>,
|
|
||||||
body: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct HexBlockHeaderDeserialize {
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
hash: Vec<u8>,
|
|
||||||
version: u32,
|
|
||||||
height: u64,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
prev_hash: Vec<u8>,
|
|
||||||
timestamp: u64,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
output_mr: Vec<u8>,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
block_output_mr: Vec<u8>,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
kernel_mr: Vec<u8>,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
input_mr: Vec<u8>,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
total_kernel_offset: Vec<u8>,
|
|
||||||
nonce: u64,
|
|
||||||
pow: HexProofOfWorkDeserialize,
|
|
||||||
kernel_mmr_size: u64,
|
|
||||||
output_mmr_size: u64,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
total_script_offset: Vec<u8>,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
validator_node_mr: Vec<u8>,
|
|
||||||
validator_node_size: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct HexProofOfWorkDeserialize {
|
|
||||||
pow_algo: u64,
|
|
||||||
#[serde(deserialize_with = "deserialize_hex_to_bytes")]
|
|
||||||
pow_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将16进制JSON转换回Block
|
|
||||||
fn convert_hex_to_block(hex_block: &HexBlockDeserialize) -> Result<Block> {
|
|
||||||
let header = if let Some(ref h) = hex_block.header {
|
|
||||||
Some(minotari_app_grpc::tari_rpc::BlockHeader {
|
|
||||||
hash: h.hash.clone(),
|
|
||||||
version: h.version,
|
|
||||||
height: h.height,
|
|
||||||
prev_hash: h.prev_hash.clone(),
|
|
||||||
timestamp: h.timestamp,
|
|
||||||
output_mr: h.output_mr.clone(),
|
|
||||||
block_output_mr: h.block_output_mr.clone(),
|
|
||||||
kernel_mr: h.kernel_mr.clone(),
|
|
||||||
input_mr: h.input_mr.clone(),
|
|
||||||
total_kernel_offset: h.total_kernel_offset.clone(),
|
|
||||||
nonce: h.nonce,
|
|
||||||
pow: Some(minotari_app_grpc::tari_rpc::ProofOfWork {
|
|
||||||
pow_algo: h.pow.pow_algo,
|
|
||||||
pow_data: h.pow.pow_data.clone(),
|
|
||||||
}),
|
|
||||||
kernel_mmr_size: h.kernel_mmr_size,
|
|
||||||
output_mmr_size: h.output_mmr_size,
|
|
||||||
total_script_offset: h.total_script_offset.clone(),
|
|
||||||
validator_node_mr: h.validator_node_mr.clone(),
|
|
||||||
validator_node_size: h.validator_node_size,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将body从JSON值转换回原始结构
|
|
||||||
let body = if let Some(ref body_json) = hex_block.body {
|
|
||||||
serde_json::from_value(body_json.clone()).map_err(|e| anyhow!("Body deserialization error: {}", e))?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Block {
|
|
||||||
header,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZMQ消息结构
|
// ZMQ消息结构
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MiningTask {
|
pub struct MiningTask {
|
||||||
|
@ -389,6 +207,46 @@ impl GbtClient {
|
||||||
Ok(node_conn)
|
Ok(node_conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 计算output_smt_size
|
||||||
|
fn calculate_output_smt_size(&self, block: &CoreBlock, prev_output_smt_size: u64) -> Result<u64> {
|
||||||
|
// 创建JellyfishMerkleTree用于计算
|
||||||
|
let mock_store = MockTreeStore::new(true);
|
||||||
|
let output_smt = JellyfishMerkleTree::<_, SmtHasher>::new(&mock_store);
|
||||||
|
|
||||||
|
let mut batch = Vec::new();
|
||||||
|
|
||||||
|
// 处理所有输出(添加新的叶子节点)
|
||||||
|
for output in block.body.outputs() {
|
||||||
|
if !output.is_burned() {
|
||||||
|
let smt_key = KeyHash(
|
||||||
|
output.commitment.as_bytes().try_into().expect("commitment is 32 bytes")
|
||||||
|
);
|
||||||
|
let smt_value = output.smt_hash(block.header.height);
|
||||||
|
batch.push((smt_key, Some(smt_value.to_vec())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理所有输入(删除叶子节点)
|
||||||
|
for input in block.body.inputs() {
|
||||||
|
let smt_key = KeyHash(
|
||||||
|
input.commitment()?.as_bytes().try_into().expect("Commitment is 32 bytes")
|
||||||
|
);
|
||||||
|
batch.push((smt_key, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算SMT变化
|
||||||
|
let (_, changes) = output_smt
|
||||||
|
.put_value_set(batch, block.header.height)
|
||||||
|
.map_err(|e| anyhow!("SMT calculation error: {}", e))?;
|
||||||
|
|
||||||
|
// 计算新的output_smt_size
|
||||||
|
let mut size = prev_output_smt_size;
|
||||||
|
size += changes.node_stats.first().map(|s| s.new_leaves).unwrap_or(0) as u64;
|
||||||
|
size = size.saturating_sub(changes.node_stats.first().map(|s| s.stale_leaves).unwrap_or(0) as u64);
|
||||||
|
|
||||||
|
Ok(size)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_block_template_and_coinbase(&mut self) -> Result<MiningTask> {
|
pub async fn get_block_template_and_coinbase(&mut self) -> Result<MiningTask> {
|
||||||
info!(target: LOG_TARGET, "Getting new block template");
|
info!(target: LOG_TARGET, "Getting new block template");
|
||||||
|
|
||||||
|
@ -419,13 +277,6 @@ impl GbtClient {
|
||||||
.ok_or_else(|| anyhow!("No header in block template"))?
|
.ok_or_else(|| anyhow!("No header in block template"))?
|
||||||
.height;
|
.height;
|
||||||
|
|
||||||
// 获取output_smt_size
|
|
||||||
let output_smt_size = block_template
|
|
||||||
.header
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| anyhow!("No header in block template"))?
|
|
||||||
.output_smt_size;
|
|
||||||
|
|
||||||
// 获取挖矿数据
|
// 获取挖矿数据
|
||||||
let miner_data = template_response
|
let miner_data = template_response
|
||||||
.miner_data
|
.miner_data
|
||||||
|
@ -469,21 +320,37 @@ impl GbtClient {
|
||||||
body.kernels.push(coinbase_kernel.into());
|
body.kernels.push(coinbase_kernel.into());
|
||||||
|
|
||||||
// 获取完整的区块
|
// 获取完整的区块
|
||||||
let block_result = self.base_node_client.get_new_block(block_template).await?.into_inner();
|
let block_result = self.base_node_client.get_new_block(block_template.clone()).await?.into_inner();
|
||||||
let block = block_result.block.ok_or_else(|| anyhow!("No block in response"))?;
|
let block = block_result.block.ok_or_else(|| anyhow!("No block in response"))?;
|
||||||
|
|
||||||
// 计算coinbase哈希
|
// 计算coinbase哈希
|
||||||
let coinbase_hash = coinbase_output.hash().to_hex();
|
let coinbase_hash = coinbase_output.hash().to_hex();
|
||||||
|
|
||||||
// 使用自定义的16进制序列化
|
// 将gRPC Block转换为CoreBlock以便计算output_smt_size
|
||||||
let hex_block = convert_block_to_hex(&block)?;
|
let core_block: CoreBlock = block.clone().try_into()
|
||||||
let block_template_json = serde_json::to_string(&hex_block).map_err(|e| anyhow!("Serialization error: {}", e))?;
|
.map_err(|e| anyhow!("Block conversion error: {}", e))?;
|
||||||
|
|
||||||
|
// 获取前一个区块的output_smt_size(从区块模板头中获取)
|
||||||
|
let prev_output_smt_size = block_template
|
||||||
|
.header
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("No header in block template"))?
|
||||||
|
.output_smt_size;
|
||||||
|
|
||||||
|
// 计算新的output_smt_size
|
||||||
|
let calculated_output_smt_size = self.calculate_output_smt_size(&core_block, prev_output_smt_size)?;
|
||||||
|
|
||||||
|
info!(target: LOG_TARGET, "Calculated output_smt_size: {} (prev: {})",
|
||||||
|
calculated_output_smt_size, prev_output_smt_size);
|
||||||
|
|
||||||
|
// 序列化区块模板
|
||||||
|
let block_template_json = serde_json::to_string(&block).map_err(|e| anyhow!("Serialization error: {}", e))?;
|
||||||
|
|
||||||
let mining_task = MiningTask {
|
let mining_task = MiningTask {
|
||||||
coinbase_hash,
|
coinbase_hash,
|
||||||
height,
|
height,
|
||||||
target: target_difficulty,
|
target: target_difficulty,
|
||||||
output_smt_size,
|
output_smt_size: calculated_output_smt_size, // 使用计算出的值
|
||||||
block_template: block_template_json,
|
block_template: block_template_json,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -541,17 +408,9 @@ impl GbtClient {
|
||||||
|
|
||||||
// 提交区块到BaseNode
|
// 提交区块到BaseNode
|
||||||
pub async fn submit_block_to_base_node(&mut self, submit_request: &SubmitRequest) -> Result<SubmitBlockResponse> {
|
pub async fn submit_block_to_base_node(&mut self, submit_request: &SubmitRequest) -> Result<SubmitBlockResponse> {
|
||||||
// 反序列化区块数据(支持16进制格式)
|
// 反序列化区块数据
|
||||||
let block: Block = if submit_request.block_data.contains("\"hash\":") {
|
let block: Block = serde_json::from_str(&submit_request.block_data)
|
||||||
// 尝试解析为16进制格式
|
.map_err(|e| anyhow!("Block deserialization error: {}", e))?;
|
||||||
let hex_block: HexBlockDeserialize = serde_json::from_str(&submit_request.block_data)
|
|
||||||
.map_err(|e| anyhow!("Hex block deserialization error: {}", e))?;
|
|
||||||
convert_hex_to_block(&hex_block)?
|
|
||||||
} else {
|
|
||||||
// 尝试解析为原始Base64格式(向后兼容)
|
|
||||||
serde_json::from_str(&submit_request.block_data)
|
|
||||||
.map_err(|e| anyhow!("Block deserialization error: {}", e))?
|
|
||||||
};
|
|
||||||
|
|
||||||
info!(target: LOG_TARGET, "Submitting block to base node for height {}", submit_request.height);
|
info!(target: LOG_TARGET, "Submitting block to base node for height {}", submit_request.height);
|
||||||
|
|
||||||
|
@ -667,17 +526,8 @@ struct Args {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// 初始化日志系统
|
// 初始化日志
|
||||||
let log_config_path = PathBuf::from("log4rs_gbt.yml");
|
|
||||||
if log_config_path.exists() {
|
|
||||||
// 使用log4rs配置文件
|
|
||||||
let base_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
||||||
tari_common::initialize_logging(&log_config_path, &base_path, "")
|
|
||||||
.map_err(|e| anyhow!("Failed to initialize logging: {}", e))?;
|
|
||||||
} else {
|
|
||||||
// 回退到env_logger
|
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
}
|
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue