从peer命令行参数到Transaction对象

以Invoke Transaction的构造为例. 分析基于v6.0版本的代码.

构造一个Transaction对象的流程

Transaction对象定义在fabric/protos/fabric.pb.go中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Transaction struct {
Type Transaction_Type `protobuf:"varint,1,opt,name=type,enum=protos.Transaction_Type" json:"type,omitempty"`
// store ChaincodeID as bytes so its encrypted value can be stored
ChaincodeID []byte `protobuf:"bytes,2,opt,name=chaincodeID,proto3" json:"chaincodeID,omitempty"`
Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"`
Metadata []byte `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
Txid string `protobuf:"bytes,5,opt,name=txid" json:"txid,omitempty"`
Timestamp *google_protobuf.Timestamp `protobuf:"bytes,6,opt,name=timestamp" json:"timestamp,omitempty"`
ConfidentialityLevel ConfidentialityLevel `protobuf:"varint,7,opt,name=confidentialityLevel,enum=protos.ConfidentialityLevel" json:"confidentialityLevel,omitempty"`
ConfidentialityProtocolVersion string `protobuf:"bytes,8,opt,name=confidentialityProtocolVersion" json:"confidentialityProtocolVersion,omitempty"`
Nonce []byte `protobuf:"bytes,9,opt,name=nonce,proto3" json:"nonce,omitempty"`
ToValidators []byte `protobuf:"bytes,10,opt,name=toValidators,proto3" json:"toValidators,omitempty"`
Cert []byte `protobuf:"bytes,11,opt,name=cert,proto3" json:"cert,omitempty"`
Signature []byte `protobuf:"bytes,12,opt,name=signature,proto3" json:"signature,omitempty"`
}

构造一个Transcation对象的关键在于如何填充Payload字段. 对于Type为Invoke的Transaction来说, Payload字段是一个Marshal过的ChaincodeInvationSpec对象, 该对象在fabric/protos/chaincode.pb.go中定义, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
type ChaincodeInvocationSpec struct {
ChaincodeSpec *ChaincodeSpec `protobuf:"bytes,1,opt,name=chaincodeSpec" json:"chaincodeSpec,omitempty"`
// This field can contain a user-specified ID generation algorithm
// If supplied, this will be used to generate a ID
// If not supplied (left empty), sha256base64 will be used
// The algorithm consists of two parts:
// 1, a hash function
// 2, a decoding used to decode user (string) input to bytes
// Currently, SHA256 with BASE64 is supported (e.g. idGenerationAlg='sha256base64')
IdGenerationAlg string `protobuf:"bytes,2,opt,name=idGenerationAlg" json:"idGenerationAlg,omitempty"`
}

里面的ChaincodeSpec在同一个文件下面:

1
2
3
4
5
6
7
8
9
10
type ChaincodeSpec struct {
Type ChaincodeSpec_Type `protobuf:"varint,1,opt,name=type,enum=protos.ChaincodeSpec_Type" json:"type,omitempty"`
ChaincodeID *ChaincodeID `protobuf:"bytes,2,opt,name=chaincodeID" json:"chaincodeID,omitempty"`
CtorMsg *ChaincodeInput `protobuf:"bytes,3,opt,name=ctorMsg" json:"ctorMsg,omitempty"`
Timeout int32 `protobuf:"varint,4,opt,name=timeout" json:"timeout,omitempty"`
SecureContext string `protobuf:"bytes,5,opt,name=secureContext" json:"secureContext,omitempty"`
ConfidentialityLevel ConfidentialityLevel `protobuf:"varint,6,opt,name=confidentialityLevel,enum=protos.ConfidentialityLevel" json:"confidentialityLevel,omitempty"`
Metadata []byte `protobuf:"bytes,7,opt,name=metadata,proto3" json:"metadata,omitempty"`
Attributes []string `protobuf:"bytes,8,rep,name=attributes" json:"attributes,omitempty"`
}

其中最核心的字段为CTorMsgChaincodeId. ChiancodeId为chaincode的路径,CTorMsg是一个指向ChaincodeInput对象的指针, 而ChiancodeInput就是调用Chaincode时指定的函数名以及参数.

因此构造一个Invoke类型的Transaction的关键步骤为:

1
CTorMsg, ChaincodeID==>ChaincodeSpec==>ChaincodeInvocationSpec==>Transaction

peer客户端是如何把命令行参数构造成相应Transaction对象的

peer使用了cobra框架, 对应chaincode子命令的相应定义在fabric/peer/chaincode/chaincode.go. 通过分析发现与invoke操作挂钩的是fabric/peer/chaincode/invoke.go里面的chaincodeInvoke, 而这个函数又转而直接调用了fabric/peer/chaincode/common.go里的chaincodeInvokeOrQuery. 关键的部分从这里开始.

函数先是调用了getChaincodeSpecification来构造一个ChaincodeSpec.

1
spec, err := getChaincodeSpecification(cmd)

跟到getChaincodeSpecification里, 可以看到在这里ChaincodeSpec是如何被填充的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spec := &pb.ChaincodeSpec{}
if err := checkChaincodeCmdParams(cmd); err != nil {
return spec, err
}
// Build the spec
input := &pb.ChaincodeInput{}
if err := json.Unmarshal([]byte(chaincodeCtorJSON), &input); err != nil {
return spec, fmt.Errorf("Chaincode argument error: %s", err)
}
var attributes []string
if err := json.Unmarshal([]byte(chaincodeAttributesJSON), &attributes); err != nil {
return spec, fmt.Errorf("Chaincode argument error: %s", err)
}
chaincodeLang = strings.ToUpper(chaincodeLang)
spec = &pb.ChaincodeSpec{
Type: pb.ChaincodeSpec_Type(pb.ChaincodeSpec_Type_value[chaincodeLang]),
ChaincodeID: &pb.ChaincodeID{Path: chaincodePath, Name: chaincodeName},
CtorMsg: input,
Attributes: attributes,
}

其中的chaincodeCtorJSON等变量就是从命令行传入的JSON格式的参数, 这个函数把这些参数Unmarshal之后填入了目标chaincodeSpec中.

得到chaincodeSpec之后,回到chaincodeInvokeOrQuery,可以看到这个chaincodeSpec被用以勾造了一个ChaincodeInvocationSpec:

1
invocation := &pb.ChaincodeInvocationSpec{ChaincodeSpec: spec}

按照上一节说的构造顺序, 接下来只要把这个ChaincodeInvocationSpec给marshal了然后扔进一个Transaction里就大功告成了. 而这个动作是在fabric/core/devops.gocreateExecTx函数中完成的(这里实际上省略了很长一段调用链的分析, 大概流程为: grpc调用invoke方法->invoke_handler->devopsServer.invoke->createExecTx).

到了createExecTx思路实际上就很清晰了, 里面有关键性的一行:

1
tx, err = pb.NewChaincodeExecute(spec, uuid, t)

其中uuid是用util.GenerateIdWithAlg由spec中的CtorMsg算出来的, t就是Transaction类型.也就是说, pb.NewChaincodeExecute完成了最终的Transaction组装.它位于fabridc/protos/transaction.go, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func NewChaincodeExecute(chaincodeInvocationSpec *ChaincodeInvocationSpec, uuid string, typ Transaction_Type) (*Transaction, error) {
transaction := new(Transaction)
transaction.Type = typ
transaction.Txid = uuid
transaction.Timestamp = util.CreateUtcTimestamp()
cID := chaincodeInvocationSpec.ChaincodeSpec.GetChaincodeID()
if cID != nil {
data, err := proto.Marshal(cID)
if err != nil {
return nil, fmt.Errorf("Could not marshal chaincode : %s", err)
}
transaction.ChaincodeID = data
}
data, err := proto.Marshal(chaincodeInvocationSpec)
if err != nil {
return nil, fmt.Errorf("Could not marshal payload for chaincode invocation: %s", err)
}
transaction.Payload = data
return transaction, nil
}

至此, 整个流程完成.

写个爬虫爬好莱坞八卦新闻

这篇文章提到的源码可以在我的github^0上面找到, 项目名称是whosdatedwho_crawler.

这篇东西是什么鬼


以下是废话.
所有东西的存在都是有原因的. 就算你是在发呆, 也有“我什么事情都不想干”这个原因. 而之所以会有这篇文章, 全都是因为我太懒了.
学校有一门奇怪的课程叫思政课社会实践, 说白了就是社会调查. 很多人都可以周期性的感知到这门课的存在, 因为每隔一段时间在你的qq聊天列表的某个群里就会蹦出来一个“社会调查问卷, 帮忙填一下, 谢谢!!!”, 而且后面一定会跟着一个红包. 但是我是懒的, 而且我也很穷. 不可能会去设计这么一份问卷然后一个一个群用红包去拜托别人. 但是作业还是要做的. 且我的良心不允许我作假. 于是, 我就希望能找到这样一个调查课题, 它所需的信息可以很方便的获取到. 而这个时候, 刚好某老实巴交的男演员被人绿了, 而且他一被绿全中国都知道了, 这让我得到了一个结论:

名人的婚恋八卦是互联网上最不缺乏的资源.

于是, 我想起了一个神奇的网站www.whosdatedwho.com^1, 这上面收录了大量(大概翻了一下, 发现深不见底, 一直到19世纪60年代的记录都有)好莱坞明星(其实也不限好莱坞, 比如秦凯跟何姿的engagement上面也有记录)的恋爱, 分手, 订婚, 结婚和生小孩的信息, 而且过半数的条目里面都会有关于这对couple的各种信息对比, 包括括国籍, 身高, 职业, 年龄等, 可以说是丰富的八卦来源. 于是, 我便开始着手编写爬虫, 把上面的数据爬下来. 有了这玩意, 就可以研究诸如”身高差对好莱坞明星恋情长度的关系”, “年龄差对好莱坞明星恋情长度的关系”之类的问题了.
这篇文章记录了寻找whosdatedwho.com的记录查询接口, 分析网页结构, 数据表设计以及构建爬虫的过程.
废话到此结束.

寻找whosdatedwho.com的记录查询接口


我们爬一个网站的时候, 并不是乱爬的, 理想的情况是:这个网站有一个可翻页的列表, 翻页可以通过改变url中的page之类的参数来控制, 每一页有固定数目的通向详情页面的链接. 这样的话, 我们的爬虫就只需要一页一页地把详情页面的链接抠出来, 然后再跟着这些链接进入到详情页面去爬取需要的信息.
在whosdatedwho的首页中间可以看到一个大大的”Latest Events”, 旁边跟着一个小小的”See More”. 直觉告诉我们这个see more里面会有我们想要得东西. 进去之后发现到了http://www.whosdatedwho.com/timeline, 而这里的确有一个列表, 但是是下拉加载更多的那种. 这个下拉加载更多的玩意儿对于爬虫来说并没有什么用处, 除非我在爬虫里面加上js解释器, 然后再向页面发送模拟的下拉信息, 不过这样太洒了, 我不打算这样做. 我们知道, 凡是这种东西肯定是用ajax或者websocket做出来的, 所以我们只要分析一下源码找到相应的数据获取接口就行了. 逛了了一圈发现这个无限延长的列表在`#ff-7c01f37bfcc5c8dc3e81f5359fd5b212 > div.ff > div.ff-latest-list, 于是我在这个节点上加了一个on subtree modifications断点, 然后下拉, 发现代码停在这样一个地方:

1
2
3
4
5
6
7
append: function() {
return this.domManip(arguments, function(a) {
if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) {
var b = m(this, a);
=> b.appendChild(a)
}
})

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(anonymous function) (js?f=/dev/zurb/…formatted:2589)
domManip (js?f=/dev/zurb/…formatted:2681)
append (js?f=/dev/zurb/…formatted:2586)
_.fn.(anonymous function) (js?f=/dev/zurb/…formatted:2702)
opts.loading.start (js?f=/static/jq…js,sly.js…:272)
infscr_retrieve (js?f=/static/jq…js,sly.js…:305)
infscr_scroll (js?f=/static/jq…js,sly.js…:308)
(anonymous function) (js?f=/static/jq…js,sly.js…:268)
dispatch (js?f=/dev/zurb/…formatted:2251)
q.handle (js?f=/dev/zurb/…formatted:2143)
trigger (js?f=/dev/zurb/…formatted:2225)
(anonymous function) (js?f=/dev/zurb/…formatted:2487)
each (js?f=/dev/zurb/…:formatted:622)
each (js?f=/dev/zurb/…:formatted:521)
trigger (js?f=/dev/zurb/…formatted:2486)
(anonymous function) (js?f=/static/jq…js,sly.js…:312)

看了以下变量a的值, 发现这是一个img节点, 图像内容是一个圈圈, 看来是下拉的时候出现的那个表示“加载中”的圈圈. 这不是我们想要的, 于是我们让它继续执行. 很快, 代码又停在了同样的地方, 但是此时调用栈已经变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(anonymous function) (js?f=/dev/zurb/…formatted:2589)
domManip (js?f=/dev/zurb/…formatted:2681)
append (js?f=/dev/zurb/…formatted:2586)
_.fn.(anonymous function) (js?f=/dev/zurb/…formatted:2702)
(anonymous function) (js?f=/static/jq…js,sly.js…:639)
each (js?f=/dev/zurb/…:formatted:622)
each (js?f=/dev/zurb/…:formatted:521)
update (js?f=/static/jq…js,sly.js…:639)
(anonymous function) (js?f=/static/jq…5.js,sly.js…:1)
opts.callback (js?f=/static/jq…js,sly.js…:274)
infscr_loadcallback (js?f=/static/jq…js,sly.js…:294)
infscr_ajax_callback (js?f=/static/jq…js,sly.js…:303)
k (js?f=/dev/zurb/…formatted:1720)
fireWith (js?f=/dev/zurb/…formatted:1777)
c (js?f=/dev/zurb/…formatted:3482)
(anonymous function) (js?f=/dev/zurb/…formatted:3744)

ajax四个大字映入眼帘. 进到到infscr_ajax_callback, 发现代码停在这个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case 'html':
instance._debug('Using ' + (method.toUpperCase()) + ' via $.ajax() method');
$.ajax({
url: desturl,
dataType: opts.dataType,
complete: function infscr_ajax_callback(jqXHR, textStatus) {
condition = (typeof (jqXHR.isResolved) !== 'undefined') ? (jqXHR.isResolved()) : (textStatus === 'success' || textStatus === 'notmodified');
if (condition) {
=> instance._loadcallback(box, jqXHR.responseText, desturl)
} else {
instance._error('end')
}
}
});
break;

看来就是这里没跑了. 查看desturl的值, 发现是?page=2&_block=page.latestEvents. 也就是说, 这个page=2就是翻页的关键. 于是我把2改成3试了一下, 发现可以正常加载新的内容. (我也尝试过把&_block=page.latestEvents去掉, 不过发现去掉之后不能正常工作.)
于是, 们得到了想要的东西:

第$k$页记录的url为: http://www.whosdatedwho.com/timeline?page=$k$&_block=page.latestEvents

设计数据表


在确认了数据能够抓取之后, 在确定下一步抓取计划之前, 需要确定你要抓取些什么, 以及在何处抓取. 通过观察, 可以发现列表页面有的信息为:

  • 事件类型, 包括: Hookup, Breakup, Engagement, Marriage, Child, Divorce
  • 日期
  • 双方姓名
  • 进入到详情页面的链接

随便进入到一个详情页面, 发现里面能够提供的信息有:

  • 姓名
  • 年龄
  • 身高
  • 星座
  • 职业
  • 发色
  • 瞳色
  • 国籍
  • 匹配度
  • 是否事实
  • 持续时间

数据用传统的关系型数据库存储, 可以设计如下数据表:

whosdatedwho
+--------------+-------------+------+-----+---------+-------+
| Field        | Type        | Null | Key | Default | Extra |
+--------------+-------------+------+-----+---------+-------+
| name1        | varchar(64) | YES  |     | NULL    |       |
| name2        | varchar(64) | YES  |     | NULL    |       |
| gender1      | varchar(8)  | YES  |     | NULL    |       |
| gender2      | varchar(8)  | YES  |     | NULL    |       |
| height1      | smallint(6) | YES  |     | NULL    |       |
| height2      | smallint(6) | YES  |     | NULL    |       |
| age1         | tinyint(4)  | YES  |     | NULL    |       |
| age2         | tinyint(4)  | YES  |     | NULL    |       |
| zodiac1      | varchar(16) | YES  |     | NULL    |       |
| zodiac2      | varchar(16) | YES  |     | NULL    |       |
| occupation1  | varchar(64) | YES  |     | NULL    |       |
| occupation2  | varchar(64) | YES  |     | NULL    |       |
| haircolor1   | varchar(32) | YES  |     | NULL    |       |
| haircolor2   | varchar(32) | YES  |     | NULL    |       |
| eyecolor1    | varchar(32) | YES  |     | NULL    |       |
| eyecolor2    | varchar(32) | YES  |     | NULL    |       |
| nationality1 | varchar(64) | YES  |     | NULL    |       |
| nationality2 | varchar(64) | YES  |     | NULL    |       |
| event        | varchar(64) | YES  |     | NULL    |       |
| date         | date        | YES  |     | NULL    |       |
| duration     | tinyint(4)  | YES  |     | NULL    |       |
| score        | tinyint(4)  | YES  |     | NULL    |       |
+--------------+-------------+------+-----+---------+-------+

相应的SQL语句为:

1
2
3
4
5
6
7
8
9
10
11
create table whosdatedwho (name1 varchar(64), name2 varchar(64),
gender1 varchar(8), gender2 varchar(8),
height1 smallint, height2 smallint,
age1 tinyint, age2 tinyint,
zodiac1 varchar(16), zodiac2 varchar(16),
occupation1 varchar(64), occupation2 varchar(64),
haircolor1 varchar(32), haircolor2 varchar(32),
eyecolor1 varchar(32), eyecolor2 varchar(32),
nationality1 varchar(64), nationality2 varchar(64),
event varchar(64), date date,
duration tinyint, score tinyint);

构建抓取xpath


我们采用xpath来定位信息承载节点, 这个东西纯粹是个体力活. 以详情页面中的couple comparison部分为例, 在#ff-couple-comparison中, 可以看到name相关的部分是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
<div class="row collapse">
<div style="color:#c6c6c6;" class="small-12 columns text-center">Name</div>
</div>
<div class="row collapse">
<div style="padding-right:1rem;border-right:1px solid #d6d6d6;" class="small-6 columns text-right">
<h5></h5>
<h5> Cassie Ventura </h5>
</div>
<div style="padding-left:1rem;border-left:1px solid #d6d6d6;" class="small-6 columns text-left">
<h5></h5>
<h5> Sean Combs </h5>
</div>
</div>
...

其结构特点为: 由一个担任标题作用的#row collapse引导出接下来的几个包含信息的#row collapse, 信息藏在一对h5标签之后. 虽然每个页面有的信息不尽相同, 但是我们可以通过遍历这些#row collapse来判断出现了哪些信息. 比如上面出现了Name, 就可以知道下一个#row collapse肯定包含了名字信息, 所以我就可以在下一个#row collapse中提取相关信息, 然后再从下下个#row collapse开始继续往下遍历, 直至遍历完这一系列#row collapse.
需要注意的一点是, couple comparison中男女出现的位置是是不一定的, 有时男的在左, 有时女的在左, 有时男的既在左又在右, 所以需要进行判断, 判断的方法是读取height一栏中图片的alt属性(Male or Female).
具体的xpath可以参见源码.

构建爬虫


其实到了这一步基本上已经水到渠成了. 爬虫使用python+scrapy实现. 需要注意的一点是, 并不是每一个item都包含了所有的字段, 所以在向数据库插入数据时不能简单地用一个字符串模板来构建sql语句, 需要根据有的字段动态构建, 代码如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
field_list = []
value_list = []
for key in item:
field_list.append(key)
value = item.fields[key]["serializer"](item[key])
if isinstance(value, unicode):
value_list.append(cymysql.escape_string(value))
elif isinstance(value, int):
value_list.append(str(value))
self.cur.execute("insert into whosdatedwho(%s) values(%s)"%
(",".join(field_list,
",".join(value_list)))
...

还有就是要注意转义, 别自己把自己给注入了.

实际爬取


接下来只需要crapy crawl whosdatedwho就可以去找个阴凉的地方喝茶慢慢等了. 爬虫的速度还是很不错的, 睡了个午觉, 起来来玩了会儿手机, 就有了33330条记录, 从1995到2016, 对于这种过家家的作业而言, 这3万多条数据足够了.

21天精通RSA(误)

这个系列是MIT Open Courseware Mathematics for Computer Science^1的记录

同余

a与b模n同余, 记作$a \equiv b(mod \space n)$, 如同字面上的意思, 它表示$rem(a, n) = rem(b, n)$. 等价地, 它还表示$n|(a-b)$.
考察$a = kn + rem(a, n)$, 发现$n|rem(a, n)-a$, 那么就有:

  1. $a \equiv rem(a, n)(mod \space n)$

同余关系是等价关系, 因此自反, 传递且对称. 除此之外, 以下性质值得注意:

  1. $a \equiv b (mod \space n)\implies a+c \equiv b+c(mod \space n)$
  2. $a \equiv b (mod \space n)\implies ac \equiv bc(mod \space n)$
  3. $a \equiv b (mod \space n) \land c \equiv d (mod \space n) \implies a+b \equiv c+d(mod \space n)$
  4. $a \equiv b (mod \space n) \land c \equiv d (mod \space n) \implies ac \equiv bd(mod \space n)$

这里只是证明一下4, 而3的证明类似, 其余的比较明显.

$a \equiv b (mod \space n) \implies ac \equiv bc(mod \space n)$
$c \equiv d (mod \space n) \implies bc \equiv bd(mod \space n)$
$由传递性, ac \equiv bd(mod \space n)$

事实上, 在我瞎折腾的过程中, 发现由传递性还可以得到另一个有趣的结果. 如果p, q互质, 那么有$tp \equiv 1(mod \space q)$, 此时如果有另一个数r满足$(r \equiv p(mod \space q)$的话, 由于$(tr \equiv tp(mod \space q)$, 故$tr \equiv 1(mod \space q)$, 也就是r, q互质.
也就是说. 如果p, q互质, 且r与p模q同余, 则, r与q也互质.

这里只有$+-\times$, 但是却没有$/$. 考察$ka \equiv kb(mod \space n)$, 什么情况下能够把k从两边消掉呢.
之前说过$a \equiv b(mod \space n) \iff n|(a-b)$, 那么也有$ka \equiv kb(mod \space n) \iff n|k(a-b)$.
也就是说只要能保证$n|k(a-b) \implies n|(a-b)$就能保证$ka \equiv kb(mod \space n) \implies a \equiv b(mod \space n)$.
那么有一点是肯定的, 那就是n和k如果互质(relatively prime)的话, 那么必然满足上述条件, 当然也存在其它满足的情况, 有兴趣的可以深究, 但是现在我们只要知道以下事实就够了.

  1. $如果a, b互质, 有ka \equiv kb(mod \space n) \implies a \equiv b(mod \space n)$

前一篇文章^2曾经说过, gcd(a, b)实际上是a和b的最小线性组合. 而a, b互质又可以写成gcd(a, b)=1, 亦即$\exists t, a, 使得ta+sb=1$, 也就是说$ta \equiv 1(mod \space b)$.
我们定义t为a在mod b条件下的乘法逆元. 如此一来, 我们就有:

  1. $如果a, n互质, 则存在a在mod \space n条件下的乘法逆元$

欧拉函数

介绍完同余, 来看看欧拉函数.
欧拉函数$\phi(n)$定义为小于n且于n互质的数的个数.
对于质数p, 显然有$\phi(p)=p-1$.
然后再来考察一堆互质的数p, q, 令r=pq. 定义比n小且与n互质的数的集合为Zn.
由p, q互质可知, 对于任意的一个$k \in Zr$, 都存在$ap + bq=k(mod \space r)$, 而且在mod r的语境下可以将a调整在(0, q), b调整到(0, p). 显然满足上述区间的a, b唯一(考虑$(a+mq)p+(b-mq-tq)p=k$), 而且a跟q互质, b跟p互质(否则与$k \in Zr$矛盾), 亦即$a \in Zq \land b \in Zp$. 于是有一个从$Zr$到$Zq \times Zp$的双射, 于是$\phi(pq) = \phi(p) \times \phi(q)$. 特别地, 如果p, q都是质数, 则$\phi(pq) = (p-1)(q-1)$. (事实上, 还可以用中国余剩定理反过来建立映射, 也可以证明).
由于合数可以唯一地表示为质数幂的乘积(算术基本定理), 一个自然的想法是研究质数幂的计算方法, 这样就可以构造出任意欧拉函数的计算方法了.
考察质数的幂$p^k$, 由于比p小且与p不互质的数有$(p, 2p, \space \cdots \space, (p^{k-1}-1)p)$共$p^{k-1}$个, 于是$\phi(p^{k})=p^k-p^{k-1}$.
那么就得到任何数的欧拉函数计算方法:

  1. $\phi(n)=n\times\prod\limits_{p_i \in n的质因数} (1- \frac{1}{p_i})$

(其实推了那么多对于RSA并没有什么卵用, 只是推着玩玩, 真正有用的只有”如果p, q都是质数, 则$\phi(pq) = (p-1)(q-1)$”这一句)

费马小定理

介绍欧拉函数之后之后, 就可以开始介绍费马小定理了. 费马小定理是这样说的:

$gcd(p, n)=1 \implies p^{\phi(n)} \equiv 1(mod \space n)$

这看起来就有点吓人了, 但其实有了前面的铺垫, 证明起来还是很简单的. 首先来证明一个小结论.
设比n小而且与n互质的数为$A={n_1, n_2, \space\cdots\space, n_r}$, 其中$r=\phi(n)$. 再构造一个数列$B={rem(pn_1, n), rem(pn_2, n), \space\cdots\space, rem(pn_r, n)}$. 显然有$(rem(pn_1, n) < n-1) \land (gcd(rem(pn_1, n), n)=1) \land (B中元素两两不同)$.
综合这三个条件可以得出: B是A的一个全排列.
那么就有:

$n_1 \cdot n_2 \cdots n_r$
$=rem(pn_1, n)\cdot rem(pn_2, n) \cdots rem(pn_r, n)$
$\equiv pn_1 \cdot \cdots \cdot pn_r$
$=p^{\phi(n)}\cdot n_1 \cdot n_2 \cdots n_r(mod \space n)$
$消去, 得1 \equiv p^{\phi(n)}(mod\space n)$

RSA

那么终于来到了最后一步, 也就是传说中的RSA. 我们先来给出RSA的加密解密步骤, 然后再来证明其正确性.
步骤是抄来的, 懒得翻译了.

  1. Generate two distinct primes, p and q. Since they can be used to
    generate the secret key, they must be kept hidden.
  2. Let n = pq.
  3. Select an integer e such that $gcd(ed, (p-1)(q-1)) = 1$.
    The public key is the pair (e, n). This should be distributed widely.
  4. Compute d such that $de \equiv 1(mod\space (p-1)(q-1)$. This can be done using the Pulverizer.
    The secret key is the pair (d, n). This should be kept hidden!
  5. Encoding: Given a message m, the sender first checks that gcd(m, n) = 1 The
    sender then encrypts message m to produce m’ using the public key:
    $m’ = rem(m^e, n)$
  6. Decoding: The receiver decrypts message m’ back to message m using the secretkey:
    $m = rem((m’)^d, n)$

因为m比n小, 所以要证明这一堆东西的正确性, 只需要证明$(m’)^d \equiv m(mod \space n)$.
有了前面的铺垫, 证明变得及其简单:

$m’ = rem(m^e, n) \implies m’ \equiv m^e(mod\space n) \implies (m’)^d \equiv m^{ed}(mod\space n)$
$de \equiv 1(mod\space (p-1)(q-1) \implies de=1+r(p-1)(q-1)$
$所以, (m’)^d \equiv m\times m^{r(p-1)(q-1)}(mod\space n)$
$\implies (m’)^d \equiv m(mod \space n)$