2024年9月

本文介绍基于
Python
语言,针对一个
文件夹
下大量的
Excel
表格文件,对其中的
每一个文件
加以操作——将其中
指定的若干列
的数据部分都向上移动一行,并将所有操作完毕的
Excel
表格文件中的数据加以合并,生成一个新的
Excel
文件的方法。

首先,我们明确一下本文的需求。在一个文件夹内,有大量的
Excel
表格文件(以
.csv
格式文件为例),其中每一个文件都有着类似如下图所示的数据特征;我们希望,对于下图中紫色框内的列,其中的
数据部分
(每一列都有一个
列名
,这个列名不算
数据部分
)都向上提升一行(比如原本数据部分的第
2
行变到第
1
行,原本第
3
行变到第
2
行,以此类推)。

image

由上图也可以看到,
需要加以数据操作的列
,有的在原本数据部分的第
1
行就没有数据,而有的在原本的数据部分中第
1
行也有数据;对于后者,我们在数据向上提升一行之后,相当于原本第
1
行的数据就被覆盖掉了。此外,很显然在每一个文件的操作结束后,
加以处理的列
的数据部分的
最后一行
肯定是没有数据的,因此在合并全部操作后的文件之前,还希望将每一个
操作后文件

最后一行
删除。

知道了需求,我们就可以开始代码的撰写;具体代码如下。

# -*- coding: utf-8 -*-
"""
Created on Fri May 19 01:47:06 2023

@author: fkxxgis
"""

import os
import pandas as pd

original_path = "E:/01_Reflectivity/25_2022Data_New"
result_path = "E:/01_Reflectivity/99_Model/02_Extract_Data/26_Train_Model_New"

result_df = pd.DataFrame()

for file in os.listdir(original_path):
    if file.endswith(".csv"):
        
        df = pd.read_csv(os.path.join(original_path, file))
        columns_move_index = list(range(8, 16)) + list(range(17, 36))
        for columns_index in columns_move_index:
            for i in range(len(df) - 1):
                df.iat[i, columns_index] = df.iat[i + 1, columns_index]
        if len(df):
            df = df.drop(len(df) - 1)
        # df = df.iloc[ : , 1 : ]
        result_df = pd.concat([result_df, df])
        
result_df.to_csv(os.path.join(result_path, "Train_Model_0715_Main.csv"), index = False)

其中,
original_path
表示存放有多个待处理的
Excel
表格文件的文件夹路径,
result_path
则是结果
Excel
表格文件的存放路径。

首先,我们通过
result_df = pd.DataFrame()
创建一个空的
DataFrame
,用于保存处理后的数据。接下来,遍历原始文件夹中的所有文件,并找到文件夹内以
.csv
结尾的文件;随后,读取这些
.csv
文件,并将其保存到
df
中。

其次,我们通过
columns_move_index = list(range(8, 16)) + list(range(17, 36))
指定需要移动数据的列的索引范围,并随后遍历需要移动数据的列。接下来的
df.iat[i, columns_index] = df.iat[i + 1, columns_index]
表示将当前行的数据替换为下一行对应的数据。

接下来,我们通过
if len(df):
判断是否
DataFrame
不为空,如果是的话就删除
DataFrame
中的最后一行数据;随后,将处理后的
DataFrame
连接到
result_df
中。

最后,我们通过
result_df.to_csv()
函数,将最终处理后的
DataFrame
保存为一个新的
Excel
表格文件,从而完成我们的需求。

至此,大功告成。

在7月15日发出
求救信
后快2个月了,很多园友出手相救——买会员、买周边、献捐助、送赞助,让救园走在希望的田野上,非常感谢每一位出手相救的园友!

在这救园期间,很多园友提出了很多很好的商业化与发展建议,我们会结合园子的实际情况与发展进展,参考大家的建议。

在这救园期间,有投资人过来谈投资,有企业过来谈合作,其中有一些已经有比较大的进展,但前提是我们先要熬过这个难关。也有好几家过来谈收购,但是虽然当前如此困难,园子依然想独立发展。

在这个救园期间,有大厂想帮助园子,准备在开发者生态建设方面和园子长期合作,但前提同样是我们先要熬过这个难关。

8月20日,我们试着上线了终身会员,没有有吸引力的终身会员权益,只有终身会员群与赠送T恤的微不足道,却引出了救园过程中的最大惊喜——截止9月8日13:00已有103会员园友成为了终身会员(详见
感谢博文
),其中终身PLUS会员19位,终身VIP会员84位。非常感谢这103园友坚定的鼎力支持!

现在已经是9月,到了救园季的最后一个月,目前来看,最大的惊喜也是最大的希望,所以今天正式发布终身会员计划。

终身会员有名额限制,终身VIP会员最多256人,终身PLUS会员最多也是256人,一共512人。

终身会员限时开放,购买截止时间是2024年9月30日23:00。

终身VIP会员价格:¥999,终身PLUS会员价格:¥3999

购买方式:在
https://cnblogs.vip
页面底部点击“终身会员”大按钮

现有年度会员可以补差价升级为终身会员,比如您已¥99购买了年度VIP会员,只需支付¥900就可以升级为终身VIP会员。

已过期的年度会员也可以补差价购买终身会员,比如您可以先以¥999购买终身VIP会员,然后通过
企业微信
联系我们,提供之前的会员购买记录,我们会把之前购买会员的费用退给您。

终身会员(终身VIP会员与终身PLUS会员)除了享有对应的年度会员的所有权益,还享受以下特有的终身会员权益:

  • 终身会员微信群:由于终身会员的长久性,这个微信群变得更有意义;
  • 终身会员内部网站:方便终身会员内部交流、讨论、共享,提供私有代码托管空间(9月底之前先用gitlab完成搭建);
  • 赠送博客园T恤:3年内每年赠送2件博客园T恤,首年购买终身会员后即赠;
  • VIP会员赠送名额:终身VIP会员可以向自己的朋友赠送3个VIP会员,终身PLUS会员可以向自己的朋友赠送10个VIP会员;
  • 终身会员证书:如果终身会员需要,可以提供园子主体公司盖章的终身会员证书,购买终身会员后1个月内可提供;
  • 推荐工作机会:终身会员如果需要找工作,园子可以通过自己的资源帮助终身会员推荐工作机会;
  • 博客园团队优先招聘:如果园子团队招人,会优先在终身会员中招聘;
  • 众包平台优惠:如果终身会员在园子的众包平台接单,园子在提成时给与优惠,终身VIP会员可享8折优惠,终身PLUS会员可享5折优惠;
  • 虚拟商品费用减免:如果园子推出付费订阅产品或者付费课程,终身VIP会员可享999元费用减免,终身PLUS会员可享3999元费用减免。

我们会继续考虑更多终身会员权益,也欢迎大家提出建议。

如果终身会员名额满了,说明救园成功了。如果终身会员名额未满,还要看年度会员、周边、捐助、赞助的收入情况。

如果救下了园子,明年我们将会给大家带来新园子。

如果熬过这次二十年的大难关,我们相信会迎来接下来二十年与开发者共赢的大发展。

任务编排(Task Orchestration)是指管理和控制多个任务的执行流程,确保它们
按照预定的顺序正确执行

1.为什么需要任务编排?

在复杂的业务场景中,任务间通常存在依赖关系,也就是某个任务会依赖另一个任务的执行结果,在这种情况下,我们需要通过任务编排,来确保任务按照正确的顺序进行执行。

例如,以下任务的执行顺序:

其中,任务二要等任务一执行完才能执行,而任务四要等任务二和任务三全部执行完才能执行。

2.任务编排实现

任务编排和控制的主要手段有以下:

  • Future
  • CompletableFuture
  • CountDownLatch
  • Semaphore
  • CyclicBarrier

但如果是全局线程池,想要实现精准的任务编排,只能使用 Future 或 CompletableFuture。

2.1 Future 任务编排

使用 Future 实现上述 4 个任务的编排(任务二要等任务一执行完才能执行,而任务四要等任务二和任务三全部执行完才能执行):

import java.util.concurrent.*;
import java.util.Arrays;

public class TaskOrchestrator {
    public static void main(String[] args) {
        // 创建一个线程池来执行任务
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 定义任务一
        Future<String> taskOneResult = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(2000); // 模拟耗时操作
                return "Task One Result";
            }
        });

        // 定义任务二,依赖任务一
        Future<String> taskTwoResult = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                String result = taskOneResult.get(); // 阻塞等待任务一完成
                Thread.sleep(1000); // 模拟耗时操作
                return "Task Two Result, got: " + result;
            }
        });

        // 定义任务三
        Future<String> taskThreeResult = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(1500); // 模拟耗时操作
                return "Task Three Result";
            }
        });

        // 定义任务四,依赖任务二和任务三
        Future<String> taskFourResult = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                String taskTwoOutput = taskTwoResult.get(); // 阻塞等待任务二完成
                String taskThreeOutput = taskThreeResult.get(); // 阻塞等待任务三完成
                Thread.sleep(500); // 模拟耗时操作
                return "Task Four Result, got: " + taskTwoOutput + " and " + taskThreeOutput;
            }
        });

        // 打印最终结果
        try {
            System.out.println("Final Result: " + taskFourResult.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

2.2 CompletableFuture 任务编排

CompletableFutrue 提供的方法有很多,但最常用和最实用的核心方法只有以下几个:

接下来,使用 CompletableFuture 实现上述 4 个任务的编排(任务二要等任务一执行完才能执行,而任务四要等任务二和任务三全部执行完才能执行):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureExample {

    public static void main(String[] args) {
        // 任务一:返回 "Task 1 result"
        CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
            return "Task 1 result";
        });
        // 任务二:依赖任务一,返回 "Task 2 result" + 任务一的结果
        CompletableFuture<String> task2 = task1.handle((result1, throwable) -> {
            try {
                // 模拟耗时操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
            return "Task 2 result " + result1;
        });
        // 任务三:和任务一、任务二并行执行,返回 "Task 3 result"
        CompletableFuture<String> task3 = CompletableFuture.supplyAsync(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(800); // 任务三可能比任务二先完成
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
            return "Task 3 result";
        });
        // 任务四:依赖任务二和任务三,等待它们都完成后执行,返回 "Task 4 result" + 任务二和任务三的结果
        CompletableFuture<String> task4 = CompletableFuture.allOf(task2, task3).handle((res, throwable) -> {
            try {
                // 这里不需要显式等待,因为 allOf 已经保证了它们完成
                return "Task 4 result with " + task2.get() + " and " + task3.get();
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
        });
        // 获取任务四的结果并打印
        String finalResult = task4.join();
        System.out.println(finalResult);
    }
}

课后思考

Future 和 CompletableFutrue 有什么关系?CompletableFutrue 底层是如何实现的?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

无论您的提示和模型有多好,一次性获得完美图像的情况很少见。

修复小缺陷的不可或缺的方法是图像修复(inpainting)。在这篇文章中,我将通过一些基本示例来介绍如何使用
图像修复
来修复缺陷。

需要的软件

我们将使用 AUTOMATIC1111 Stable Diffusion GUI 来创建图像。

基本的图像修复设置

在这一部分,我将逐步展示如何使用图像修复来修复小缺陷。

我们先使用下面的提示来创建一张图片:

正向提示词:

masterpiece,best quality,masterpiece,best quality,official art,extremely detailed CG unity 8k wallpaper,a beautiful woman,full body,

负向提示词:

lowers,monochrome,grayscales,skin spots,acnes,skin blemishes,age spot,6 more fingers on one hand,deformity,bad legs,error legs,bad feet,malformed limbs,extra limbs,

我们可以得到下面的图片:

image-20240703201556791

虽然这张图片整体上看起来还不错,但是还是有一些问题。

比如脸部和手部。

那么接下来我们怎么修复呢?

选择对应的模型

如果你经常浏览C站的话,你可以看到对于有些模型会有一种专门给重绘使用的模型,这种模型是专门为图像修复而训练的Stable Diffusion模型。

如果您想获得最佳结果,可以使用它。但通常,使用生成图像的相同模型进行图像修复也是可以的。

我们把对应的模型下载下来,并将其放入文件夹中:

stable-diffusion-webui/models/Stable-diffusion

在AUTOMATIC1111中,点击左上角检查点选择下拉框旁边的刷新图标,就可以看到你刚刚下载的模型了。

创建图像修复遮罩

在AUTOMATIC1111 GUI中,选择
img2img
标签并选择
Inpaint
子标签。将图像上传到图像修复画布。

或者在txt2img标签中选择send img to inpaint。

我们将同时修复手部和脸部。使用画笔工具创建一个
遮罩
。这是您希望Stable Diffusion重新生成图像的区域。

image-20240703202118650

图像修复的设置

图像大小

需要调整图像大小以与原始图像相同。(在这种情况下为768 x 512)。

image-20240703202219551

面部修复

如果您正在修复面部,可以打开restore faces。选择对应的face restoration model:CodeFormer。

image-20240703202529557

有朋友会问了,为什么我的页面上面没有restore faces选项呢?

如果你没有这个选项的话,需要到setttings里面的user interface添加下面的两个设置:

image-20240703202501548

请注意,此选项可能会生成不自然的外观。它也可能生成与模型风格不一致的内容。

遮罩内容

下一个重要设置是
Masked Content

如果您希望结果由原始内容的颜色和形状引导,请选择
original

original通常用于面部图像修复
,因为一般形状和解剖结构是正确的。我们只是希望它看起来有点不同。

在大多数情况下,您将使用
original
并更改
去噪强度
以实现不同的效果。

如果您想要从原始图像中完全重新生成某些内容,例如移除一个肢体或隐藏一只手,可以使用
latent noise

latent nothing

这些选项使用与原始图像不同的内容初始化遮罩区域。它将产生完全不同的东西。

去噪强度

去噪强度
控制与原始图像相比将进行多少变化。当您将其设置为0时,什么都不会改变。当您将其设置为1时,您将获得一个不相关的图像。0.75通常是一个很好的起点。如果您想要更少的变化,请降低它。

批量大小

确保一次生成一些图像,以便您可以选择最好的。将
种子
设置为-1,以便每个图像都不同。

图像修复结果

以下是一些修复后的图像。

image-20240703204340543

可以看到第四张还是不错的,但是还不够完美。所以我们可以考虑再来一轮修复。

再进行一轮图像修复

把上面生成的最后一张图片再发到inpait中再次修复。

我们可以得到下面的结果:

image-20240703204936210

图像修复是一个迭代过程。您可以根据需要多次应用它来细化图像。

如果一次不行的话,我们可以考虑多来几次。

添加新对象

有时,您可能希望在图像中添加一些新东西。

让我们尝试在图片中添加一把剑。

首先,将图像上传到图像修复画布并在手部的位置添加遮罩。

在原始提示的开头添加“holding a sword”。图像修复的提示是

(holding a sword:1.5),masterpiece,best quality,masterpiece,best quality,official art,extremely detailed CG unity 8k wallpaper,a beautiful woman,full body,

向原始提示中添加新对象确保风格一致。您可以调整关键词权重(上面的1.5)以使宝剑显示。


遮罩内容
设置为
潜在噪声

调整
去噪强度

CFG比例
以微调修复后的图像。

经过一些实验,我们的任务完成了:

image-20240703210315083

图像修复参数的解释

去噪强度

去噪强度控制最终图像和原始内容的相似度。将其设置为0则什么都不会改变。将其设置为1,则您会得到一个不相关的图像。

如果您想要小的变化,请设置为低值;如果您想要大的变化,请设置为高值。

CFG scale

类似于在文本到图像中的使用,
CFG scale
是一个参数,用于控制模型和你的提示词的关联度。

1 – 大致忽略您的提示。

3 – 更有创造力。

7 – 在遵循提示和自由之间取得良好的平衡。

15 – 更多地遵循提示。

30 – 严格遵循提示。

遮罩内容

遮罩内容控制遮罩区域是如何初始化的。

fill
:用原始图像的高度模糊版本初始化。

Original
:未修改。

latent noise
:遮罩区域用
填充
初始化,并在潜在空间中添加随机噪声。

latent nothing
:像潜在噪声,只是没有在潜在空间中添加噪声。

图像修复的技巧

成功的图像修复需要耐心和技巧。以下是使用图像修复的一些要点:

  • 一次修复一个小区域。
  • 尝试不同的
    遮罩内容
    以查看哪个最有效。
  • 可以多次尝试修复。
  • 如果在AUTOMATIC1111的设置中什么都不起作用,请使用像Photoshop或GIMP这样的图像编辑软件,用您想要的大致形状和颜色绘制感兴趣的区域。上传那张图像并用原始内容进行图像修复。

点我查看更多精彩内容:www.flydean.com

管理什么密钥?

在区块链应用的基础组件中通常有这样一种功能,需要持续不断的向区块链中发送交易,比如arbitrum的
Sequencer
需要持续不断的发送L2的区块,
stark
需要发送单步证明/rBlock发布 的交易,chainlink需要定时发送datafeed交易。而这每一笔交易都需要L1上的账户做签名,如何安全的使用和管理这个密钥是值得关心的。

结论

就我所看到的一般有两种方式:

  • 通过配置文件配置私钥
  • 使用filekey的方式:
    • 注意file一般需要一个密码,密码是启动后在终端控制台输入

当然,密钥管理不只是简单的将密钥注入到程序里面,而是如何在程序里面安全的使用这些密钥,毕竟如果密钥发在一个可能被外部接口调用的接口中,可能会降低密钥的安全性。

密钥安全等级(依次递减,只考虑加密算法公开的情况):

  • 黑客无法得知任何明文&密文
  • 黑客可以得到密文
  • 黑客可以得到密文对应的明文
  • 黑客可以自行构造明文产生密文

所以在程序中也需要对密钥进行保护。

以太坊中每次使用完私钥会将私钥的地址还原成0地址,就是为了避免私钥在内存中泄漏。

私钥泄漏的原理大致是,geth程序在使用玩内存后会释放内存,而他释放内存并不会把内存值全部置0,而只是告诉操作系统,“这段内存我不用了,你可以分配给别的程序” 。而别的程序申请到这段内存之后,他是可以直接读取这段内存里的值,(经典案例就是 在c语言中如果你初始化一个变量,而不为赋值,那他的值不是0值,而是原先在这个值里面的内存的值)

arbitrum的处理方案

先从最底层的调用开始看

在单步证明的调用中可以看到,这笔交易的用户信息是保存在auth字段中的

func (m *ChallengeManager) IssueOneStepProof(
	ctx context.Context,
	oldState *ChallengeState,
	startSegment int,
) (*types.Transaction, error) {
	position := oldState.Segments[startSegment].Position
	proof, err := m.executionChallengeBackend.GetProofAt(ctx, position)
	if err != nil {
		return nil, fmt.Errorf("error getting OSP from challenge %v backend at step %v: %w", m.challengeIndex, position, err)
	}
	return m.challengeCore.con.OneStepProveExecution(
		m.challengeCore.auth,   // 用户信息保存在这个字段
		m.challengeCore.challengeIndex,
		challengegen.ChallengeLibSegmentSelection{
			OldSegmentsStart:  oldState.Start,
			OldSegmentsLength: new(big.Int).Sub(oldState.End, oldState.Start),
			OldSegments:       oldState.RawSegments,
			ChallengePosition: big.NewInt(int64(startSegment)),
		},
		proof,
	)
}

具体如何使用可以继续点进去看,最终是auth中包含一个变量(函数类型的变量),由这个变量进行签名,(我们需要找到的这个函数的生命周期,也就是密钥的生命周期)。

那么继续往上看,看这个challengeManager的构造方法

func NewChallengeManager(
	ctx context.Context,
	l1client bind.ContractBackend,
	auth *bind.TransactOpts,
	fromAddr common.Address,
	challengeManagerAddr common.Address,
	challengeIndex uint64,
	val *StatelessBlockValidator,
	startL1Block uint64,
	confirmationBlocks int64,
) (*ChallengeManager, error) {
	...
	return &ChallengeManager{
		challengeCore: &challengeCore{
			con:                  con,
			challengeManagerAddr: challengeManagerAddr,
			challengeIndex:       challengeIndex,
			client:               l1client,
			auth:                 auth,   // 也就是上面的auth
			actingAs:             fromAddr,
			startL1Block:         new(big.Int).SetUint64(startL1Block),
			confirmationBlocks:   confirmationBlocks,
		},
		blockChallengeBackend: backend,
		validator:             val,
		wasmModuleRoot:        challengeInfo.WasmModuleRoot,
		maxBatchesRead:        challengeInfo.MaxInboxMessages,
	}, nil
}

可以看到auth是上面传递过来的bind.ContractOpts

继续往上面看,auth来自与Builder这个结构,好在这个结构的构造函数只被调用过一次(我们及假设唯一的构造得到的auth就是我们要找的auth,中间没有发生更改)

func NewBuilder(wallet ValidatorWalletInterface) (*Builder, error) {
	randKey, err := crypto.GenerateKey()
	if err != nil {
		return nil, err
	}
	builderAuth := wallet.AuthIfEoa()
	var isAuthFake bool
	if builderAuth == nil {
		// Make a fake auth so we have txs to give to the smart contract wallet
		builderAuth, err = bind.NewKeyedTransactorWithChainID(randKey, big.NewInt(9999999))
		if err != nil {
			return nil, err
		}
		isAuthFake = true
	}
	return &Builder{
		builderAuth: builderAuth,
		wallet:      wallet,
		L1Interface: wallet.L1Client(),
		isAuthFake:  isAuthFake,
	}, nil
}

builder的auth有两种途径,一种是 AuthIfEoa 也就是从eoa中解析私钥,一种是自己生成私钥

那么关键在与这里的wallet是什么(也就是现在从跟踪auth转移到跟踪wallet)

最终发现wallet在creatNoteImpl方法里面构造的

var wallet staker.ValidatorWalletInterface = validatorwallet.NewNoOp(l1client, deployInfo.Rollup)
if !strings.EqualFold(config.Staker.Strategy, "watchtower") {
	if config.Staker.UseSmartContractWallet || (txOptsValidator == nil && config.Staker.DataPoster.ExternalSigner.URL == "") {// 合约账户
		var existingWalletAddress *common.Address
		if len(config.Staker.ContractWalletAddress) > 0 {
			if !common.IsHexAddress(config.Staker.ContractWalletAddress) {
				log.Error("invalid validator smart contract wallet", "addr", config.Staker.ContractWalletAddress)
				return nil, errors.New("invalid validator smart contract wallet address")
			}
			tmpAddress := common.HexToAddress(config.Staker.ContractWalletAddress)
			existingWalletAddress = &tmpAddress
		}
		wallet, err = validatorwallet.NewContract(dp, existingWalletAddress, deployInfo.ValidatorWalletCreator, deployInfo.Rollup, l1Reader, txOptsValidator, int64(deployInfo.DeployedAt), func(common.Address) {}, getExtraGas)
		if err != nil {
			return nil, err
		}
	} else {
		if len(config.Staker.ContractWalletAddress) > 0 {
			return nil, errors.New("validator contract wallet specified but flag to use a smart contract wallet was not specified")
		}
		wallet, err = validatorwallet.NewEOA(dp, deployInfo.Rollup, l1client, getExtraGas)
		if err != nil {
			return nil, err
		}
	}
}

继续跟踪我们得到wellet中的验证方法是由txOptsValidator提供的

向上继续找txOptsValidator

最重找到mainImpl

	if sequencerNeedsKey || nodeConfig.Node.BatchPoster.ParentChainWallet.OnlyCreateKey {
		l1TransactionOptsBatchPoster, dataSigner, err = util.OpenWallet("l1-batch-poster", &nodeConfig.Node.BatchPoster.ParentChainWallet, new(big.Int).SetUint64(nodeConfig.ParentChain.ID))
		if err != nil {
			flag.Usage()
			log.Crit("error opening Batch poster parent chain wallet", "path", nodeConfig.Node.BatchPoster.ParentChainWallet.Pathname, "account", nodeConfig.Node.BatchPoster.ParentChainWallet.Account, "err", err)
		}
		if nodeConfig.Node.BatchPoster.ParentChainWallet.OnlyCreateKey {
			return 0
		}
	}
	if validatorNeedsKey || nodeConfig.Node.Staker.ParentChainWallet.OnlyCreateKey {
		l1TransactionOptsValidator, _, err = util.OpenWallet("l1-validator", &nodeConfig.Node.Staker.ParentChainWallet, new(big.Int).SetUint64(nodeConfig.ParentChain.ID))
		if err != nil {
			flag.Usage()
			log.Crit("error opening Validator parent chain wallet", "path", nodeConfig.Node.Staker.ParentChainWallet.Pathname, "account", nodeConfig.Node.Staker.ParentChainWallet.Account, "err", err)
		}
		if nodeConfig.Node.Staker.ParentChainWallet.OnlyCreateKey {
			return 0
		}
	}

我们得到l1TransactionOptsValidator是使用
nodeConfig.Node.Staker.ParentChainWallet
这个配置项得到的.

最终的数据结构张这个样子

type WalletConfig struct {
	Pathname      string `koanf:"pathname"`
	Password      string `koanf:"password"`
	PrivateKey    string `koanf:"private-key"`
	Account       string `koanf:"account"`
	OnlyCreateKey bool   `koanf:"only-create-key"`
}

继续点到OpenWallet可以看到他是如何处理这些配置项的

在有私钥的情况下最终会走到这个方法

func NewKeyedTransactorWithChainID(key *ecdsa.PrivateKey, chainID *big.Int) (*TransactOpts, error) {
	keyAddr := crypto.PubkeyToAddress(key.PublicKey)
	if chainID == nil {
		return nil, ErrNoChainID
	}
	signer := types.LatestSignerForChainID(chainID)
	return &TransactOpts{
		From: keyAddr,
		Signer: func(address common.Address, tx *types.Transaction) (*types.Transaction, error) {  // signer就是我们一直再找的在发送交易时使用到的签名方法
			if address != keyAddr {
				return nil, ErrNotAuthorized
			}
			signature, err := crypto.Sign(signer.Hash(tx).Bytes(), key)
			if err != nil {
				return nil, err
			}
			return tx.WithSignature(signer, signature)
		},
		Context: context.Background(),
	}, nil
}

从这里看出来,私钥始终保存在signer这个方法中,在整个使用过程中没有将私钥作为参数传递的情况。

如果使用的是filekey+密码的情况会进入到这个方法

func NewKeyStoreTransactor(keystore *keystore.KeyStore, account accounts.Account) (*TransactOpts, error) {
	log.Warn("WARNING: NewKeyStoreTransactor has been deprecated in favour of NewTransactorWithChainID")
	signer := types.HomesteadSigner{}
	return &TransactOpts{
		From: account.Address,
		Signer: func(address common.Address, tx *types.Transaction) (*types.Transaction, error) {
			if address != account.Address {
				return nil, ErrNotAuthorized
			}
			signature, err := keystore.SignHash(account, signer.Hash(tx).Bytes())
			if err != nil {
				return nil, err
			}
			return tx.WithSignature(signer, signature)
		},
		Context: context.Background(),
	}, nil
}

程序会根据filekey构造一个keystore,后续签名都是在keystore中签名

注意filekey的密码是在终端控制台输入的,其中的readPass函数如下

func readPass() (string, error) {
	bytePassword, err := term.ReadPassword(syscall.Stdin)
	if err != nil {
		return "", err
	}
	passphrase := string(bytePassword)
	passphrase = strings.TrimSpace(passphrase)
	return passphrase, nil
}