{"content":{"title":"总结超级账本合约开发过程中遇到的坑（2022.10.19更新）","body":"# 1. 不要尝试获取节点本地的时间作为key或value的值\r\n即使每个节点的本地的时间系统是经过校对一致的，但同一个交易在每个节点上执行的时间点不一定是相同的。如果使用time.now().unix()来取节点本地的时间作为key或value的值，会导致每个节点执行同一个交易的结果不一致，从而造成交易失败。正确的做法是使用函数GetTxTimestamp()来获取交易的时间。每个节点通过函数GetTxTimestamp()来获取到的交易时间是相同的\r\n例子：\r\nt, err := stub.GetTxTimestamp()\r\nt.Seconds // 交易的时间，unix时间戳，精确到秒\r\nt.Nanos// 交易的时间，unix时间戳，精确到纳秒\r\ntime.Unix(t.Seconds, int64(t.Nanos)).UnixNano() / 1e6 // 交易的时间，unix时间戳，精确到毫秒\r\n\r\n但需要注意的是，每个节点通过函数GetTxTimestamp()来获取到的交易时间是相同的，但它取到的时间是客户端创建交易时的本地时间。如果客户端的本地时间没有校准的话，GetTxTimestamp()获取到的交易时间也是不准确的。\r\n\r\n\r\n# 2. 在同一个区块里多笔交易对同一键进行更新\r\n对同一键进行更新的一个或多笔交易打包到同一块中，只有第1笔交易会成功，后面的交易会失败。相关的原因是：在一个区块里的多笔交易，只有在区块被提交后才会真正生效。而每一个键值都有版本号，假设一个键“key_1”在本个区块提交前的版本号为9，然后在本个区块里有2笔交易分别更新键\"key_1\"的值为\"123\"和\"456\"。那么当第1笔交易更新键“key_1”的值时，键\"key_1\"的版本号在本个区块里将更新为10，但在本区块未提交前，真正生效的版本号还是9。而当执行第2笔交易时，该交易读取到键\"key_1\"的版本号还是9，这样第2笔交易还是把键\"key_1\"的版本号更新为10，这样系统会判断为“双花”行为，从而引起冲突，导致只有第1笔交易会执行成功，第2笔交易执行失败。具体可以参考这篇文章 https://learnblockchain.cn/article/620\r\n简单地说，在同一个区块里多笔交易对同一键进行PutState()更新时，只有一笔交易是成功的，其它的交易会出现 MVCC_READ_CONFLICT报错。\r\n因此，试图在fabric合约里创建全局的计数器是一件不可靠的事情。\r\n\r\n\r\n# 3. 在同一个区块里多笔交易先后对同一个键进行更新和读取\r\n假设在一个区块里有2笔交易，第1笔交易对键\"key_1\"值更新为\"123\"，第2笔交易尝试读取键\"key_1\"的值，结果第2笔交易读键\"key_1\"的值结果为空。原因上面已经提及到，一个区块里的多笔交易，只有在区块被提交后才会真正生效。\r\n\r\n\r\n# 4. 在一个方法内对同一个键先写后读\r\n在一个方法内对同一个键先进行写键值（putState()），然后再进行读键值（getState()），会发现前面的putState操作没有生效，原因也是同前面一样的道理\r\n\r\n\r\n# 5. 尝试通过合约使用加密算法对信息进行加密\r\n有些加密算法对同样内容的信息进行加密，有可能每次加密的结果是不一样的。例如：RSA公钥对同一数据加密，每次的结果都不一样。这样会导致不同的节点执行加密的结果不一样，从而导致提交的内容不一致，造成交易失败\r\n\r\n\r\n# 6. 没有了解query和invoke的合约函数调用方式\r\n超级账本调用合约函数的方式有2种：query和invoke\r\n通过query方式调用合约函数，只能查询链上数据，不能向链上写数据，并且不产生交易\r\n通过invoke方式调用合约函数，可以向链上写数据，并产生交易\r\n\r\n如果一个合约函数里调用了PutState()操作，但是通过query方式来调用该合约函数，虽然不会报错，但PutState()操作实际没有生效，世界状态不会被改变\r\n\r\n如果一个合约函数里没有调用PutState()操作，但是通过invoke方式来调用该合约函数，虽然不会报错，但却会产生交易\r\n\r\n\r\n# 7. 使用CouchDB，在使用select语句时没有对select的字段建立索引\r\n\r\n超级账本可以使用Leveldb或CouchDB。如果要支持富查询，一般会选择使用CouchDB。CouchDB可以支持类似于SQL的select语句来实现富查询。但是CouchDB的效率并不高，在不建立索引的情况下，可能对2-3万条的数据使用select语句就会导致查询非常耗时，从而导致合约函数的执行超时而失败。而对于传统的关系型数据库，2-3万条的数据量，在不建立索引的情况下，对查询的影响不是很明显。\r\n\r\n例如下面的select语句：\r\n\"{\"selector\":{\\\"StudentAge\\\":{\\\"$lte\\\": 10}, \\\"sort\\\":[{\\\"RegisterTime\\\": \\\"asc\\\"}]}}\"\r\n\r\n最好就需要对字段StudentAge和RegisterTime建立索引。\r\n\r\n想知道CouchDB在执行一条查询语句究竟有没有使用索引，用了哪些索引，可以通过向CouchDB发送post请求的方法，通过返回的response来查看使用了哪些索引。具体可参考下面的链接\r\nhttps://docs.couchdb.org/en/latest/api/database/find.html#db-explain\r\n1.3.8. /db/_explain 这一节的内容\r\n\r\n具体的操作可以通过postman来实现，\r\npost的地址如http://127.0.0.1:5984/member_goods/_explain\r\n发送的body就是查询select语句的内容，\r\n注意header里需要配置一个KeyValue   Content-Type:application/json\r\n\r\n\r\n# 8. 使用CouchDB，在使用select语句时尽量避免使用sort进行排序\r\nsort排序会严重消耗查询时间，容易造成查询超时，应尽量避免使用。使用sort语句，必须对sort语句中涉及的字段创建索引，否则会报错。\r\n\r\n\r\n\r\n# 9.搭建Fabric区块链浏览器的坑\r\n搭建Fabric区块链浏览器的时，如果之前曾经在同一台机器上使用不同的用户证书搭建过Fabric区块链浏览器，那么在同一台机器上使用新的用户证书搭建Fabric区块链浏览器时，一定要运行下面的命令清除之前的容器服务里的volume残留值：\r\ndocker-compose down -v\r\n\r\n详细可以参考这篇文章\r\nhttps://blog.csdn.net/lyz19961221/article/details/118576529\r\n\r\n\r\n\r\n# 10.避免Fabric的智能合约函数陷入死循环\r\n合约函数陷入死循环，不但会导致本次合约函数执行超时，而且后面会导致无法执行其它合约函数的问题，后果非常严重，详细可以参考这篇文章\r\nhttps://zhuanlan.zhihu.com/p/572782459\r\n\r\n\r\n------------------------------------------------------------------------------\r\n我是powervip\r\n我的公众号：区块链战斗机\r\n我的知乎：https://www.zhihu.com/people/powervip\r\n我的学习笔记：www.study.win\r\n\r\n\r\n**原创作品，版权所有，侵权必究！商业转载请联系作者获得授权；非商业转载需保留作者署名信息，注明出处并保留原文链接。**\r\n\r\n**如果你觉得这篇文章写得还可以，请帮忙点个赞，谢谢！\r\n你的鼓励，我的动力！**"},"author":{"user":"https://learnblockchain.cn/people/5018","address":null},"history":null,"timestamp":1666949809,"version":1}