banner
leaf

leaf

It is better to manage the army than to manage the people. And the enemy.
follow
substack
tg_channel

Cryptography of Blockchain

The password hashing function is a purely deterministic function that maps input from a large space to a fixed set of outputs. These outputs are commonly referred to as input digests. For example, the input could be the entire text of the preface of this book, and its digest could be a hexadecimal value from a 128-bit value space.

Without formalization, a secure hashing function should be used. This means that it should be nearly impossible to find two different inputs that produce the same digest. Hash functions should also be irreversible. If only the digest is provided, it should be impossible to find the input that produced the digest. Similarly, small changes to the input should result in significant changes to the output digest—two similar inputs should have very different digests. Hash functions should also be relatively fast to compute from their inputs, making it a simple task to verify that an input matches its digest.

Hash values are central to maintaining the integrity of blockchains and are the foundation of proof-of-work consensus mechanisms. We will see these two uses over several pages.

Public Key Cryptography

Public key cryptography can be implemented through many different algorithms, one of the most popular being Rivest-Shamir-Adleman (more commonly known as RSA); however, it relies on the Elliptic Curve Digital Signature Algorithm (ECDSA). This algorithm also allows for the recovery of the public key given a message and its signature.

With these two cryptographic concepts, we can finally begin learning how to build a blockchain.

A blockchain is a decentralized, immutable public digital ledger. We can think of a blockchain as a distributed database in which once a record is confirmed, it can never be deleted or modified, and no authority can control this database, which is replicated across all nodes in a peer-to-peer network. The actual content stored in the database may vary; it can be currency, asset registrations, or even executable code.

In a blockchain, each state change is part of a transaction. Transactions can be viewed as anatomical write operations from users in a global database that may change one or more arrays. Any user in the network can submit a transaction to be executed.

How to handle transfers is part of the blockchain state transition relationship. By processing each transaction it receives, the blockchain transitions from one state to another. For example, a blockchain managing currency might handle a transfer of currency between two accounts: it decreases the sender's balance and increases the recipient's balance by the same amount. Other blockchains even allow transactions to create and execute complete programs on the chain.

When a block is added to the blockchain, it propagates to all nodes through the peer-to-peer network. Each node will re-execute all transactions in the block to check their validity, and if they notice any illegal changes, they will reject the block. This means that each transaction is effectively executed once for every single byte across the entire network. This allows the blockchain to be completely decentralized, as each node checks all ongoing transactions, but this comes at a cost: the overhead limits the number of transactions that can be processed.

Note: Public blockchains do not require users to register. They simply need to create a new key pair to start signing transactions to participate in the network; however, they may require users to possess currency associated with the blockchain to process their transactions.

Transactions are processed in batches as blocks and then linked together to form the actual blockchain. These blocks constitute the history of the blockchain, with each block packing a set of transactions that change its state. The way transactions are selected and ordered within each block depends on the consensus rules of the blockchain, which we will see on later pages.

When a block is added to the blockchain, it propagates to all nodes through the peer-to-peer network. Each node will re-execute all transactions in the block to check their validity, and if they notice any illegal changes, they will reject the block. This means that each transaction is effectively executed once for every single byte across the entire network. This allows the blockchain to be completely decentralized, as each node checks all ongoing transactions, but this comes at a cost: the overhead limits the number of transactions that can be processed.

Transactions are processed in batches as blocks and then linked together to form the actual blockchain. These blocks constitute the history of the blockchain, with each block packing a set of transactions that change its state. The way transactions are selected and ordered within each block depends on the consensus rules of the blockchain, which we will see on later pages.

When a block is added to the blockchain, it propagates to all nodes through the peer-to-peer network. Each node will re-execute all transactions in the block to check their validity, and if they notice any illegal changes, they will reject the block. This means that each transaction is effectively executed once for every single byte across the entire network. This allows the blockchain to be completely decentralized, as each node checks all ongoing transactions, but this comes at a cost: the overhead limits the number of transactions that can be processed.

The network processes transactions per second. In other words, blocks are exchanged based on the number of transactions.

Given the high cost of processing changes to a blockchain, all transaction fees must be paid. These fees are typically paid in a currency native to the blockchain (such as Bitcoin in the Bitcoin network or Ether in Ethereum). Regardless of who benefits from these fees, we will see in a few pages that the goal of these fees is to prevent attackers from flooding the network with transactions that each node must process and to reward nodes that add new blocks to the chain.

Blockchains resist changes by storing a summary of their entire history on each block. Each block in the chain is identified by a hash calculated from its own transactions and the hash of the previous block.

How blocks are constructed. Each block is identified by the hash of the previous block and all its own transactions. We will see the role of "now" in the next section.

Using this scheme, any change to any transaction on any block in the chain will result in changes to all subsequent hashes, making any modification trivial. For example, if an attacker tries to change a transaction that occurred ten blocks ago, not only will the digest of that block change, but the digest of the next block will also change (since it is calculated based on the hash of the previous block), and this continues all the way to the head of the chain. However, to prevent attackers from modifying the blockchain and distributing false copies in the network, this mechanism must distinguish between the attacker regenerating all blocks. This is the source of proof of work.

Transactions are ordered, and the inclusion of transactions in the blocks of the blockchain will depend on the decay of the network. Since we are dealing with a decentralized database, we need a way for all participants to agree on how to add changes. For example, if a seller offers an asset on the blockchain and two buyers rush to purchase it, how can a decentralized network decide who is first? Worse, how can we prevent the seller from telling both buyers that they made purchases and cash transactions? "We need a way to determine how transactions are selected and ordered to maintain a single state of the blockchain." In other words, we need a method for adding blocks to the blockchain.

Many public blockchains, such as Bitcoin or Ethereum, rely on a consensus algorithm known as "proof of work." Proof of work is a cryptographic proof of a large number of CPU cycles used to perform computations; in this case, it is based on calculating a difficult number from the block. To add a block to the blockchain, it must be accompanied by proof that the node adding the block will be rewarded for their efforts. Nodes that fulfill this role are called "miners," and they compete to add the next block to receive the corresponding reward. Note that the mechanisms for these proofs are actually quite simple. The identifier for the front block in the chain is a hash that includes the identifier of the previous block, all transactions in the block, and a nonce. By changing the current value, the hash of the block will be completely different. To add a new block to the chain, a thief must have a specific structure (starting with N zeros). Since predicting what the hash will look like is not passive, miners can only compute repeatedly while changing the nonce until they find a hash that meets the requirements. This requires many attempts and is therefore considered proof of work.

Keep in mind that the entire infrastructure runs on a peer-to-peer decentralized network. This allows nodes to run and leave the network at will without requiring a centralized server. Here, the Proof-of-Work algorithm provides a way for new nodes to know which is the actual chain: they just need to look for the chain with the highest accumulated computational power.

This also prevents malicious participants from simply changing records on the chain and recalculating all subsequent block hashes, as we discussed in the previous section. To do this, an attacker would need to solve all the proof of work from the attacked blocks, which requires more computational power than other miners in the network.

Note that besides proof of work, there are other consensus mechanisms. Inside... proof of authority and proof of stake serve as alternatives for establishing faster and smaller chains.

The aging algorithm is closely related to the finality of the network. When we know that it has been included in the blockchain and will not change, we say that the exchange is final. Transactions added in recent blocks are far from being considered final: if a miner manages to mine two blocks in a row, from EXT to the last, they may generate a new chain that replaces the latest block without including that transaction.

This is called a reorganization, which is not uncommon in proof-of-work chains. To know that a transaction is final, we need to wait for several blocks to be mined above the transaction to ensure it will not change. The number of blocks will depend on the specific chain and how much confidence we need. The throughput is designed to solve a proof of work that is computationally expensive. This, in itself, enforces a dependency on blockchain throughput, forcing a difficult problem to be solved every time a large number of transactions are added. However, there is another reason that limits the number of transactions added to the chain per second: verifiability. To maintain the decentralization of the blockchain, every node in the network needs to be able to verify that each transaction is legitimate and executed according to established rules. If the network accepts a large number of transactions per second, then only powerful devices will be able to verify the chain, leaving the network inaccessible to any user without access to the necessary hardware. Therefore, low throughput is related to ensuring public access to the blockchain.

In particular, Ethereum is designed to handle about 15 transactions per second. Note that transactions in Ethereum can be more complex because they can execute arbitrary computations, so this limit is actually related to how much effort is needed to run and verify each transaction in each block.

Note that these several transactions are shared among all users and applications in the network per second. Even for a traditional web application, this is a very low limit. We will see some methods around this limit in Chapter 8.

From Bitcoin to Ethereum, so far, WWE has treated a block of data as a public database, but we have not yet delved into what that database might contain. The first famous blockchain was used to track ownership of a digital currency, Bitcoin.

The blockchain as we understand it today was largely introduced in 2008 by Satoshi Nakamoto in his paper "Bitcoin: A Peer-to-Peer Electronic Cash System." This paper is both short and easy to read, and it encapsulates most of the blockchain concepts used today. It introduced a "purely peer-to-peer version of electronic cash, allowing online payments to be sent directly from one party to another without going through a financial institution."

In summary, the Bitcoin blockchain is a public decentralized database that tracks user balances of Bitcoin and supports transactions transferring funds from one auditing entity to another, representing an implementation of a deeply centralized electronic payment platform. It is worth mentioning that, in addition to ordinary transfers, Bitcoin also supports a limited scripting language. This language allows for constructs such as time locks, which restrict execution to a future time, or multi-signature transactions, which require multiple accounts to agree to move assets. What can be built with this language is still limited. It was proposed specifically to support arbitration computations in the network.

Cryptography has a long history, primarily applied in ancient times for the transmission of military secrets, such as "passwords," "codes," etc. Before 1970, the application scope of cryptography was mostly at the government level. It was not until the invention of standard encryption systems—Data Encryption Standard and asymmetric encryption algorithms—that cryptography began to be deeply applied in various fields.

The Development of Cryptography#

The development of cryptography can be roughly divided into three stages: classical cryptography -> modern cryptography -> public key cryptography.

  1. Classical cryptography: The core cryptographic ideas of this stage mainly involve substitution and transposition. Substitution means replacing each character of plaintext with another character to produce ciphertext, which the recipient can decode back to plaintext using the corresponding character substitution. Transposition means scrambling the order of characters in the plaintext according to certain rules.
  2. Modern cryptography: The development of this stage mainly involves symmetric encryption algorithms. Symmetric encryption means that the sender uses a certain public algorithm with a key to encrypt plaintext, and the recipient uses the key provided by the sender to decrypt the ciphertext back to plaintext.
  3. Public key cryptography: The development of this stage mainly involves asymmetric encryption algorithms. The principle of asymmetric encryption is public key encryption and private key decryption. The implementation process is that A generates a pair of keys through a certain algorithm, which are the public key and the private key, and then makes the public key public. If B wants to send information to A, they use A's public key to encrypt the plaintext, producing ciphertext, and send it to A. Upon receiving the ciphertext, A uses their private key to decrypt it back to plaintext.

The diagram of asymmetric encryption and decryption is as follows:

Asymmetric encryption involves: public key and private key.

Characteristics are:

  • Characteristic 1: Data encrypted with the public key can only be decrypted with the private key; the public key cannot decrypt it.
  • Characteristic 2: Data encrypted with the private key can only be decrypted with the public key; the private key cannot decrypt it.
  • The server holds both the public key and the private key (not giving it to anyone).
  • The server gives its public key to whoever it wants to communicate with.

The interaction process using asymmetric encryption is as follows: The client first obtains the server's public key, then uses this public key to encrypt the data, and sends the encrypted data to the server. Due to the characteristics mentioned above, only the server can correctly decrypt the data.

Cryptography is widely used in blockchain and can be divided into three categories: symmetric encryption algorithms, asymmetric encryption algorithms, and hash hashing algorithms. Common methods include: Merkle tree hash algorithms, elliptic curve algorithms, SHA-256 algorithms, Base58 encoding. Their functions include: quickly searching through hash algorithms; encrypting and decrypting plaintext; signing and verifying information; generating digital certificates; generating account addresses, etc.

Symmetric encryption algorithms are the earliest applied encryption algorithms, and the technology is mature.

  1. The process of using symmetric encryption

In symmetric encryption algorithms, the data sender processes the plaintext (original data) and the encryption key together through a special encryption algorithm, transforming it into complex encrypted ciphertext to be sent out. Upon receiving the ciphertext, the recipient must use the key used for encryption and the reverse algorithm of the same algorithm to decrypt the ciphertext back to readable plaintext. In symmetric encryption algorithms, only one key is used, and both the sender and recipient use this key to encrypt and decrypt the data, which requires the decrypting party to know the encryption key in advance.

  1. Characteristics of symmetric encryption

The characteristics of symmetric encryption algorithms are that the algorithm is public, the computational load is small, the encryption speed is fast, and the encryption efficiency is high. The downside is that both parties in a transaction use the same key, which does not guarantee security. Furthermore, each pair of users must use a unique key that others do not know each time they use symmetric encryption, which causes the number of keys held by the sender and recipient to grow geometrically, making key management a burden for users. For example, if two users need to use symmetric encryption to encrypt and then exchange data, each user needs at least two keys and to exchange them. If there are n users within an organization, then the entire organization would need n × (n - 1) keys, and the generation and distribution of keys would become a nightmare for the organization's information department. The security of symmetric encryption algorithms depends on how well the encryption key is kept, but it is impossible to require every person holding a key in an organization to keep it secret; they often inadvertently leak the key—if a key used by a user is obtained by an intruder, the intruder can read all documents encrypted with that user's key. If the entire organization shares one encryption key, then the confidentiality of all organizational documents is compromised.

Symmetric encryption algorithms are relatively difficult to use in distributed network systems, mainly due to the difficulty of key management and high usage costs. Compared to public key encryption algorithms, or asymmetric encryption algorithms, symmetric encryption algorithms can provide encryption and authentication but lack signature functionality, limiting their scope of use.

  1. Classification of symmetric encryption

Symmetric encryption is divided into stream ciphers and block ciphers.

  • Stream ciphers encrypt each byte of plaintext sequentially. Encryption refers to using the user's key through some complex operation (cipher algorithm) to produce a large amount of pseudo-random flow to encrypt the plaintext stream. Decryption refers to using the same key and cipher algorithm and the same pseudo-random flow to restore the plaintext stream.
  • Block ciphers encrypt one block of plaintext at a time. It groups plaintext into fixed-length segments, and the plaintext group undergoes encryption operations to obtain a ciphertext group, which is restored to a plaintext group through decryption operations (the inverse operation of encryption).

Stream ciphers, also known as stream ciphers, use a seed key to generate a pseudo-random sequence that matches the length of the plaintext. When encrypting a message m using a stream cipher, it is encrypted byte by byte, generally first dividing m into consecutive characters, m = m1m2m3…; then using the i-th character ki from the key stream k = k1k2k3... to perform the encryption transformation on the i-th character mi of the plaintext message, i = 1, 2, 3...; all encryption outputs concatenated together form the ciphertext after encrypting m. Decryption needs to be synchronized with encryption, so when using stream ciphers, the sender and receiver need to operate on the same position of the plaintext or ciphertext.

The encryption and decryption process is illustrated as follows:

Block ciphers encrypt data in fixed-size blocks, typically 64 or 128 bits. The most common block cipher algorithms include AES, DES, and 3DES.

In block ciphers, there are logical relationships between both plaintext blocks and ciphertext blocks, which are the operational modes. The five most common operational modes for block ciphers are:

  • Electronic Code Book (ECB)
  • Cipher Block Chaining (CBC)
  • Cipher Feedback Mode (CFB)
  • Output Feedback Mode (OFB)
  • Counter mode (CTR)

Currently, CBC mode and CTR mode are recommended, while other modes are less commonly used or not recommended.

  1. ECB mode

ECB, also known as Electronic Code Book mode, is the most basic block cipher encryption mode. Before encryption, it divides the plaintext into several blocks based on the encryption block size (e.g., AES is 128 bits). If the last block is less than 128 bits, padding is used (specific to the algorithm, default is 0x00). Each block is then encrypted separately using the same key to obtain ciphertext blocks, which are then concatenated to form the final ciphertext. Decryption follows the same principle.

The following diagram illustrates the encryption and decryption process in ECB mode:

Decryption process:

From this, we can see that the same plaintext content will always be encrypted into the same ciphertext, and the format of the ciphertext is the same as that of the plaintext. This is very insecure, especially in cases where images or plaintext content is repeated many times. Since all blocks are encrypted in the same way, repeated content in the plaintext will be reflected in the ciphertext, making it difficult to resist statistical analysis attacks. Additionally, because the order of the plaintext and ciphertext content is the same, an attacker can easily disrupt the ciphertext. If an attacker intercepts the ciphertext during transmission and disrupts the order of the ciphertext content, the recipient will not be able to decrypt the ciphertext back to the original plaintext. This is also why ECB mode is rarely used.

Characteristics:

  1. Simple operation, easy to implement, conducive to parallel computation, and errors will not be transmitted.
  2. Cannot hide the pattern of the plaintext.
  3. May allow active attacks on the plaintext.

CBC mode#

CBC, also known as Cipher Block Chaining mode, is named because the ciphertext blocks are linked together like a chain.

In CBC mode, each plaintext block is first XORed with the previous ciphertext block before being encrypted. In this method, each ciphertext block depends on all previous plaintext blocks. Additionally, to ensure the uniqueness of each message, an initialization vector is required for the first block.
If the index of the first block is 1, the encryption process for CBC mode is:
Ci = Ek (P ⊕ Ci-1), C0 = IV.
The decryption process is:
Pi = Dk (Ci) ⊕ Ci-1, C0 = IV.

The operational process of CBC mode is illustrated as follows:

CBC mode operation process

Advantages of the CBC algorithm:

  1. Repeated arrangements of plaintext will not be reflected in the ciphertext.
  2. Supports parallel computation (only decryption).
  3. Can decrypt any ciphertext block.

Disadvantages of the CBC algorithm:

  1. When decrypting ciphertext containing certain erroneous bits, all bits of the first block and the corresponding bits of the next block will be erroneous.
  2. Encryption does not support parallel computation.

CFB, also known as Cipher Feedback, is similar to CBC and can turn block ciphers into self-synchronizing stream ciphers; the working process is also very similar. It requires a shift register of the same size as the block and initializes the register with IV. Then, the contents of the register are encrypted using the block cipher, and the highest x bits of the result are XORed with the plaintext's x bits to produce the x bits of ciphertext. The next x bits of ciphertext are then moved into the register, and this process is repeated for the next x bits of plaintext. The decryption process is similar to the encryption process, starting with IV, encrypting the register, and XORing the result's high x with the ciphertext to produce x bits of plaintext, then moving the next x bits of ciphertext into the register.

Similar to CBC, changes to the plaintext will affect all subsequent ciphertext, so the encryption process cannot be parallelized; similarly, the decryption process can be parallelized.

The operational process of CFB mode is illustrated as follows:

CFB mode operation process

Advantages of CFB mode:

  1. Does not require padding.
  2. Supports parallel computation (only decryption).
  3. Can decrypt any ciphertext block.

Disadvantages of CFB mode:

  1. Encryption does not support parallel computation.
  2. When decrypting ciphertext containing certain erroneous bits, all bits of the first block and the corresponding bits of the next block will be erroneous.
  3. Cannot resist replay attacks.

OFB: Runs block ciphers as synchronous stream ciphers, similar to CFB, but OFB uses the output of the previous n bits of ciphertext to feed back into the shift register, and OFB does not have error propagation issues.
Output Feedback Mode (OFB) can turn block ciphers into synchronous stream ciphers. It generates key stream blocks, which are then XORed with plaintext blocks to produce ciphertext. Like other stream ciphers, flipping one bit in the ciphertext will also flip the same position bit in the plaintext. This characteristic allows many error correction codes, such as parity bits, to be computed before encryption and still yield correct results after encryption.
Each output block used in OFB is related to all previous output blocks, so it cannot be processed in parallel. However, since plaintext and ciphertext are only used in the final XOR process, IV can be encrypted in advance, allowing for parallel processing of plaintext or ciphertext in the final XOR operation.
It is practical to use the CBC mode with an input of all zeros to generate the key stream for the OFB mode. This method is very practical because it can accelerate the encryption process of the OFB mode using fast CBC hardware implementations.

The encryption process is illustrated as follows:

OFB mode encryption process

Advantages of OFB mode:

  1. Does not require padding.
  2. Can prepare for encryption and decryption in advance.
  3. Uses the same structure for encryption and decryption.
  4. When decrypting ciphertext containing certain erroneous bits, only the corresponding bits in the plaintext will be erroneous.

Disadvantages of OFB mode:

  1. Does not support parallel computation.
  2. Actively attacking and reversing certain bits in the ciphertext will also reverse the corresponding bits in the plaintext block.

CTR mode#

Counter mode (CTR mode) encryption encrypts a series of input data blocks (called counters) to produce a series of stream ciphers, which are XORed with plaintext to obtain ciphertext. Similarly, decryption is achieved by XORing the stream cipher with the ciphertext to obtain plaintext.
Data blocks are encrypted by generating different bit sequences through incrementing counters, which consist of a nonce and a counter (block number). The CTR counter is 128 bits long (16 bytes). The first 8 bytes are called the nonce, which is different for each encryption. The last 8 bytes are the block number, which is incremented by 1. The nonce serves to complicate the content of the data block. Without the nonce, the data blocks would be too uniform. The implementation of the counter in Golang is slightly different from what is described here; it first initializes an initial vector iv of length BLOCK.SIZE(), then increments the last byte of iv through the counter for each group, which also generates different data blocks before encryption.
The encryption process involves generating an initial counter. Suppose there are 8 blocks; the initial counter is incremented to obtain 8 counter values, each of which is then encrypted to produce key streams, and each key stream is XORed with the corresponding plaintext block to produce ciphertext. Thus, its encryption process is equivalent to one-time pad encryption.

In CTR mode, blocks can be encrypted and decrypted in any order because the value of the "counter" needed for both encryption and decryption can be calculated directly from the nonce and block number. This means that parallel computation can be achieved. In systems that support parallel computation, the speed of CTR mode is very fast.
The encryption process is illustrated as follows:

unc AesCTR_Encrypt(plainText,key[]byte)[]byte{
	//判断用户传过来的key是否符合16字节,如果不符合16字节加以处理
	keylen:=len(key)
	if keylen==0{   //如果用户传入的密钥为空那么就用默认密钥
		key=[]byte("wumansgygoaescbc")   //默认密钥
	}else if keylen>0&&keylen<16{  //如果密钥长度在0到16之间,那么用0补齐剩余的
		key=append(key,bytes.Repeat([]byte{0},(16-keylen))...)
	}else if keylen>16{
		key=key[:16]
	}
	//1.指定使用的加密aes算法
	block, err := aes.NewCipher(key)
	if err!=nil{
		panic(err)
	}

	//2.不需要填充,直接获取ctr分组模式的stream
	// 返回一个计数器模式的、底层采用block生成key流的Stream接口,初始向量iv的长度必须等于block的块尺寸。
	iv := []byte("wumansgygoaesctr")
	stream := cipher.NewCTR(block, iv)

	//3.加密操作
	cipherText := make([]byte,len(plainText))
	stream.XORKeyStream(cipherText,plainText)

	return cipherText
}

func AesCTR_Decrypt(cipherText,key []byte)[]byte{
	//判断用户传过来的key是否符合16字节,如果不符合16字节加以处理
	keylen:=len(key)
	if keylen==0{   //如果用户传入的密钥为空那么就用默认密钥
		key=[]byte("wumansgygoaescbc")   //默认密钥
	}else if keylen>0&&keylen<16{  //如果密钥长度在0到16之间,那么用0补齐剩余的
		key=append(key,bytes.Repeat([]byte{0},(16-keylen))...)
	}else if keylen>16{
		key=key[:16]
	}
	//1.指定算法:aes
	block, err:= aes.NewCipher(key)
	if err!=nil{
		panic(err)
	}
	//2.返回一个计数器模式的、底层采用block生成key流的Stream接口,初始向量iv的长度必须等于block的块尺寸。
	iv := []byte("wumansgygoaesctr")
	stream := cipher.NewCTR(block, iv)

	//3.解密操作
	plainText := make([]byte,len(cipherText))
	stream.XORKeyStream(plainText,cipherText)

	return plainText
}

Asymmetric encryption, also known as public key cryptography, was first proposed by Diffie and Hellman in 1976 as a revolutionary new encryption idea. At that time, almost all cryptographic systems were symmetric, based on simple methods like substitution and transposition. Public key cryptography is completely different; it is asymmetric and involves two different keys, a public key and a private key, with encryption based on complex mathematical functions. These mathematical functions are based on mathematical problems, generally divided into three categories: large integer factorization problems, discrete logarithm problems, and elliptic curve problems. Sometimes, elliptic curve problems are classified under discrete logarithm problems. Public key cryptography represents a revolutionary change, breaking the original cryptographic model and solving two major problems of traditional cryptography: key distribution and digital signatures.

  1. Overview of Asymmetric Encryption

Traditional cryptographic systems use a single key, and the cost of transmitting that key from the sender to the receiver is high and risky. If the recipient receives the ciphertext, they cannot determine its authenticity if it has been altered during transmission. Public key systems perfectly solve these problems. They have a pair of keys: one is the public key, which is completely open and can be received by anyone; the other is the private key, which is kept secret and not shared with anyone. It is impossible to calculate the private key from the public key, so the private key is secure. The sender A encrypts the plaintext using the public key, and the recipient B decrypts it using the corresponding private key. To ensure the integrity of the transmitted ciphertext and the accuracy of the message source, the ciphertext must be digitally signed. A encrypts the ciphertext with their private key, a process called digital signing; B receives the ciphertext and decrypts it with the corresponding public key, a process called signature verification.

Thus, public key cryptography can be divided into two models: the encryption-decryption model and the signing-verification model. The two models can be used independently or together, depending on the application scenario. Generally, the transmitted ciphertext requires digital signatures, and the sent content includes both the ciphertext and the signature. The recipient first verifies the signature; if the verification passes, they then proceed to decrypt.

Many methods exist for asymmetric encryption, and the following discusses three encryption methods: RSA, DSA, and ECDSA.

Requirements for Public Key Cryptography#

To implement public key cryptography, the following requirements must be met:

  1. A pair of keys must be generated, i.e., a public-private key pair, which is computationally easy to produce.
  2. It must be computationally easy to encrypt plaintext using the public key.
  3. It must be computationally easy to decrypt ciphertext using the private key.
  4. It must be impossible to calculate the private key from the public key.
  5. It must be impossible to calculate the plaintext from the public key and ciphertext.
  6. The order of encryption and decryption can be swapped.

Currently, there are many difficult problems that meet these requirements for establishing public key cryptography. I will analyze the following two commonly used ones:

  1. Large Integer Factorization Problem
    If two large prime numbers p and q are known, calculating n = pq is easy, but if n is known, finding p and q is nearly impossible. This is the large integer factorization problem.
  2. Discrete Logarithm Problem
    First, understand two concepts: order and primitive root.
    Let m > 1 and (a, m) = 1, then the smallest positive integer t that satisfies a^t ≡ 1 mod m is called the order of a modulo m, denoted as δm(a).
    A primitive root is a mathematical symbol. If m is a positive integer and a is an integer, if the order of a modulo m equals φ(m), then a is called a primitive root modulo m. The φ(m) mentioned is the number of prime factors of m.
    Given a formula a^t mod b ≡ c, where a is a primitive root of b, b is a very large prime number, and c is a positive integer less than b. The problem is that it is easy to find t given a, b, but it is very difficult to find t given a, b, c. The process of solving this is nearly impossible, especially when large numbers are involved.

RSA Algorithm#

RSA is one of the most widely used public key cryptographic systems. It was proposed in 1977 by Ronald Rivest, Adi Shamir, and Leonard Adleman while they were working at the Massachusetts Institute of Technology. The name RSA comes from the initials of their last names. The security of the RSA algorithm is based on the difficulty of the RSA problem, which is essentially based on the difficulty of large integer factorization. However, the RSA problem is not necessarily more difficult than the factorization problem, meaning that it may be possible to solve the RSA problem without solving the factorization problem, so the RSA algorithm is not entirely based on the difficulty of large integer factorization.

  1. Description of the RSA Algorithm

1.1 Generating Public and Private Keys in RSA

Here is a specific example of how to generate a key pair:

  1. Randomly select two distinct prime numbers p and q.
    Alice chooses 61 and 53. (In practical applications, the larger these two primes are, the harder they are to crack.)
  2. Calculate the product of p and q, n.
    n = 61 × 53 = 3233
    The length of n is the key length. 3233 in binary is 110010100001, which has 12 bits, so this key is 12 bits long. In practical applications, RSA keys are generally 1024 bits long, and in important situations, they are 2048 bits long.
  3. Calculate Euler's totient function φ(n) for n, denoted as L.
    According to the formula φ(n) = (p-1)(q-1)
    Alice calculates φ(3233) to be 60 × 52, which equals 3120.
  4. Randomly choose an integer e, which is the number used to encrypt in the public key.
    The condition is 1 < e < φ(n), and e must be coprime to φ(n).
    Alice randomly selects 17 between 1 and 3120. (In practical applications, 65537 is often chosen.)
  5. Calculate the modular inverse d of e with respect to φ(n). This is the number used to decrypt in the key.
    The so-called "modular inverse" means that there is an integer d such that ed leaves a remainder of 1 when divided by φ(n). ed ≡ 1 (mod φ(n))
    Alice finds 2753, meaning 17 × 2753 mod 3120 = 1.
  6. The public key is formed by (n, e), and the private key is formed by (n, d).
    In Alice's case, n = 3233, e = 17, d = 2753, so the public key is (3233, 17), and the private key is (3233, 2753).

1.2 RSA Encryption

First, the plaintext is divided into bit string groups, ensuring that each group corresponds to a decimal number less than n. Each group m is then encrypted sequentially, and the sequence of ciphertexts forms the encrypted result, i.e., m satisfies 0 ≤ m < n, and the encryption algorithm is:
c ≡ m^e mod n; where c is the ciphertext, and 0 ≤ c < n.

1.3 RSA Decryption

For ciphertext 0 ≤ c < n, the decryption algorithm is:
m ≡ c^d mod n;

1.4 RSA Signature Verification

The RSA cryptographic system can be used for both encryption and digital signatures. Below is the process for RSA digital signatures.
Given the public key (e, n) and private key d:

  1. For a message m, the signature is: sign ≡ m^d mod n
  2. Verification: For the message signature pair (m, sign), if m ≡ sign^e mod n, then sign is a valid signature for m.

2. Implementing RSA Encryption and Decryption in Golang with Known Public and Private Keys#

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "errors"
    "fmt"
)

// Can be generated through openssl
// openssl genrsa -out rsa_private_key.pem 1024
var privateKey = []byte(`  
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDfw1/P15GQzGGYvNwVmXIGGxea8Pb2wJcF7ZW7tmFdLSjOItn9
kvUsbQgS5yxx+f2sAv1ocxbPTsFdRc6yUTJdeQolDOkEzNP0B8XKm+Lxy4giwwR5
LJQTANkqe4w/d9u129bRhTu/SUzSUIr65zZ/s6TUGQD6QzKY1Y8xS+FoQQIDAQAB
AoGAbSNg7wHomORm0dWDzvEpwTqjl8nh2tZyksyf1I+PC6BEH8613k04UfPYFUg1
0F2rUaOfr7s6q+BwxaqPtz+NPUotMjeVrEmmYM4rrYkrnd0lRiAxmkQUBlLrCBiF
u+bluDkHXF7+TUfJm4AZAvbtR2wO5DUAOZ244FfJueYyZHECQQD+V5/WrgKkBlYy
XhioQBXff7TLCrmMlUziJcQ295kIn8n1GaKzunJkhreoMbiRe0hpIIgPYb9E57tT
/mP/MoYtAkEA4Ti6XiOXgxzV5gcB+fhJyb8PJCVkgP2wg0OQp2DKPp+5xsmRuUXv
720oExv92jv6X65x631VGjDmfJNb99wq5QJBAMSHUKrBqqizfMdOjh7z5fLc6wY5
M0a91rqoFAWlLErNrXAGbwIRf3LN5fvA76z6ZelViczY6sKDjOxKFVqL38ECQG0S
pxdOT2M9BM45GJjxyPJ+qBuOTGU391Mq1pRpCKlZe4QtPHioyTGAAMd4Z/FX2MKb
3in48c0UX5t3VjPsmY0CQQCc1jmEoB83JmTHYByvDpc8kzsD8+GmiPVrausrjj4p
y2DQpGmUic2zqCxl6qXMpBGtFEhrUbKhOiVOJbRNGvWW
-----END RSA PRIVATE KEY-----
`)

// openssl
// openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
var publicKey = []byte(`  
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfw1/P15GQzGGYvNwVmXIGGxea
8Pb2wJcF7ZW7tmFdLSjOItn9kvUsbQgS5yxx+f2sAv1ocxbPTsFdRc6yUTJdeQol
DOkEzNP0B8XKm+Lxy4giwwR5LJQTANkqe4w/d9u129bRhTu/SUzSUIr65zZ/s6TU
GQD6QzKY1Y8xS+FoQQIDAQAB
-----END PUBLIC KEY-----     
`)

// Encrypt
func RsaEncrypt(origData []byte) ([]byte, error) {
    // Decode PEM format public key, obtaining the public key block
    block, _ := pem.Decode(publicKey)
    if block == nil {
        return nil, errors.New("public key error")
    }
    // Parse to obtain the public key
    pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err // Interface type assertion
    }
    pub := pubInterface.(*rsa.PublicKey)
    // Encrypt
    return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}

// Decrypt
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
    // Decode PEM format private key, obtaining the private key block
    block, _ := pem.Decode(privateKey)
    if block == nil {
        return nil, errors.New("private key error!")
    }
    // Parse to obtain PKCS1 format private key
    priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    // Decrypt
    return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}

func main() {
    data, _ := RsaEncrypt([]byte("hello world"))
    fmt.Println(base64.StdEncoding.EncodeToString(data))
    origData, _ := RsaDecrypt(data)
    fmt.Println(string(origData))
}

Implementing RSA Key Generation and Encryption/Decryption in Golang#

package main

import (
    "crypto/rsa"
    "crypto/rand"
    "fmt"
    "crypto/md5"
    "encoding/base64"
)

// Generate private key and public key through RSA
func main() {
    // RSA first generates a private key, then generates a public key based on the private key
    // Generate a 1024-bit private key
    pri, _ := rsa.GenerateKey(rand.Reader, 2048)
    // Generate public key
    pub := &pri.PublicKey
    // Define plaintext
    plaintext := []byte("hello china")
    // Encrypt into ciphertext, using OAEP padding
    ciphertext, _ := rsa.EncryptOAEP(md5.New(), rand.Reader, pub, plaintext, nil)
    fmt.Println(base64.StdEncoding.EncodeToString(ciphertext))

    // Decrypt
    plaintext, _ = rsa.DecryptOAEP(md5.New(), rand.Reader, pri, ciphertext, nil)
    fmt.Println(string(plaintext))
}

Implementing DSA Signing and Verification in Golang#

package main

import (
    "crypto/dsa"
    "crypto/rand"
    "fmt"
)

// DSA is primarily used for signing and verification
func main() {
    // DSA is specialized for signing and verification
    var param dsa.Parameters // The structure contains three very large numbers (bigInt)
    // Instantiate the structure
    dsa.GenerateParameters(&param, rand.Reader, dsa.L1024N160) // L is 1024, N is 160; here L is the private key, N is the public key initial parameter
    // Generate private key
    var priv dsa.PrivateKey // privateKey is a structure that contains the publicKey structure, which has a Parameters field
    priv.Parameters = param
    // Generate private key through random reading and parameters
    dsa.GenerateKey(&priv, rand.Reader)

    // Generate public key from private key
    pub := priv.PublicKey
    // Define plaintext
    message := []byte("hello world")
    // r, s are two integers; sign the message with the private key to obtain two random integers r and s
    r, s, _ := dsa.Sign(rand.Reader, &priv, message) // Use the public key to verify the signature, verifying r and s
    b := dsa.Verify(&pub, message, r, s)
    if b == true {
        fmt.Println("Verification successful")
    } else {
        fmt.Println("Verification failed")
    }
}

Hash (Hash) Algorithm#

A hash function is an important branch of cryptography. It is a type of mathematical function that can transform messages of arbitrary length into a fixed-length binary string in a reasonable time and is irreversible. This output value is called the hash value, also known as the hash or message digest. Hash algorithms based on hash functions have wide applications in digital signatures, achieving data integrity, Merkle tree data storage and retrieval, etc. In the Bitcoin system, two cryptographic hash functions are used: SHA256 and RIPEMD160. RIPEMD160 is mainly used to generate Bitcoin addresses, while SHA256 is the hash function for almost all cryptographic algorithms on the Bitcoin chain.

  1. Technical Principles

A hash function, also known as a hashing function, is a one-way cryptographic mechanism, meaning it can only encrypt but not decrypt. The mathematical expression can be: h = H(m), where H is the hash function, m is the information to be encrypted, and h is the output hash value of fixed length. The operation process involves setting an initial vector, padding the message to the length required by the algorithm, splitting the padded message into N data blocks, and iteratively processing the N data blocks with the initial vector through the hash algorithm to finally obtain a fixed-length hash value.
Hash functions have the following characteristics:

  1. Compression: Encrypts information of arbitrary length into a fixed-length hash value.
  2. One-way: The mathematical principle of the hash function has no inverse operation, so it cannot convert the hash value back to the original information.
  3. Collision resistance: The operation process of the hash function is quite complex, involving various mathematical operations and a large number of variable loop operations, making it nearly impossible for two different messages to produce the same hash value.
  4. High sensitivity: Any small change in the input can lead to significant changes in the output.

Typical hash functions are divided into two categories: Message Digest Algorithm (MD5) and Secure Hash Algorithm (SHA).

  1. Hash Collision

An ideal hash function produces two different hash values for different inputs. In practice, if there exist two different messages m and m' such that H(m) = H(m'), then m and m' are said to be a collision for that function. In simple terms, a hash collision refers to two different messages producing the same hash value under the action of the same hash function.
To ensure data security and immutability, the actual hash algorithm must be complex enough to have strong collision resistance.
Collision resistance is divided into two types: weak collision resistance, which means that for a specified message x and function H, it is computationally infeasible to find a message y such that H(x) = H(y); and strong collision resistance, which means that for a given function H, it is computationally infeasible to find any pair of different messages x and y such that H(x) = H(y).

SHA256

SHA is a family of cryptographic hash functions, which stands for Secure Hash Algorithm. It was designed by the National Security Agency (NSA) and published by the National Institute of Standards and Technology (NIST). The SHA family currently has three series: SHA-1, SHA-2, and SHA-3. Because SHA-1 has been shown to be vulnerable to attacks, it is now rarely used. SHA-3, produced in 2012, is also known as the Keccak algorithm and is mainly used in the Ethereum public chain.
SHA algorithms have the following characteristics: 1. It is impossible to restore the information from the message digest; 2. Two different messages will not produce the same message digest.
SHA256 is currently the most fundamental and widely used algorithm in blockchain encryption. It is the most representative encryption algorithm in the SHA-2 algorithm series. Understanding and skillfully using SHA256 is the most basic requirement for blockchain technology professionals.

  1. SHA256 Algorithm Principles

SHA-256#

SHA-256 refers to a hashing algorithm that processes messages of less than 2^64 bits in length (measured in bits) in 512-bit blocks, ultimately producing a 32-byte length data hash. The generated data is called the message digest. Due to the uniqueness and determinism of the message digest, it can be used to verify whether data has changed during transmission, i.e., to verify its integrity.

SHA-256 Process

1.1 Operation Units

The processing unit of the SHA algorithm is bits. In this article, a "word" is 32 bits, while a "byte" is 8 bits. For example, the string "abc" can be converted into a bit string: 01100001 01100010 01100011. It can also be represented as a hexadecimal string: 0x616263.

1.2 Padding

The message is converted into a binary string, and a "1" and several "0"s are added at the end to make the length modulo 512 equal to 448. For example, the padding process for the information "abc" is shown below.

Original information: 01100001 01100010 01100011
Padding step one: 01100001 01100010 01100011 1
First, add a "1"
Padding step two: 01100001 01100010 01100011 10…..0
Then add 423 "0"s

1.3 Message Filling

The padded message is then appended with a 64-bit message length information, ensuring that the filled message length is exactly a multiple of 512 bits. The appended 64-bit message length information is the bit length of the original message, and the filled message is divided into 512-bit message blocks.

1.4 Initial Vector

SHA256 is an iterative hash function based on the Merkle-Damgard structure, requiring an initial vector for the first operation. This vector is a variable throughout the operation. The initial values for SHA256 are derived from the square roots of the first eight prime numbers (2, 3, 5, 7, 11, 13, 17, 19), taking the decimal parts of these square roots for the first 32 bits. For example, the square root of 2 yields: 0.414213562373095048..., which in binary for the first 32 bits is: 10110111111100101010000101001010, and converting this to hexadecimal gives: 0x6a09e667. Similarly, other initial values can be obtained from other primes. These initial values are stored in eight registers A, B, C, D, E, F, G, and H, which are:

A = H0 = 0x6a09e667
B = H1 = 0xbb67ae85
C = H2 = 0x3c6ef372
D = H3 = 0xa54ff53a
E = H4 = 0x510e527f
F = H5 = 0x9b05688c

G = H6 = 0x1f83d9ab
H = H7 = 0x5be0cd19

1.5 The 64 Constants Used

In the SHA256 algorithm, 64 constants are used, derived from the decimal parts of the cube roots of the first 64 prime numbers. Their purpose is to provide a set of 64 random strings that can be randomly selected as parameters for changing the initial vector function for each message block operation. These 64 constants are as follows:

428a2f98 71374491 b5c0fbcf e9b5dba5 
3956c25b 59f111f1 923f82a4 ab1c5ed5 
d807aa98 12835b01 243185be 550c7dc3 
72be5d74 80deb1fe 9bdc06a7 c19bf174 
e49b69c1 efbe4786 0fc19dc6 240ca1cc 
2de92c6f 4a7484aa 5cb0a9dc 76f988da 
983e5152 a831c66d b00327c8 bf597fc7 
c6e00bf3 d5a79147 06ca6351 14292967 
27b70a85 2e1b2138 4d2c6dfc 53380d13 
650a7354 766a0abb 81c2c92e 92722c85 
a2bfe8a1 a81a664b c24b8b70 c76c51a3 
d192e819 d6990624 f40e3585 106aa070 
19a4c116 1e376c08 2748774c 34b0bcb5 
391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 
748f82ee 78a5636f 84c87814 8cc70208 
90befffa a4506ceb bef9a3f7 c67178f2

1.6 Operation Process

The operation process is briefly described as follows:

  1. Create eight variables a, b, c, d, e, f, g, h, and assign them the corresponding initial vector values.
  2. After padding and filling the original message, divide it into N blocks of 512 bits each.
  3. The operation on M involves a large loop, structured as: For i = 1 to N;
  4. Inside the large loop, there is a 64-step loop to modify the eight variables, and the final modified eight variables are used as parameters for the next large loop.
  5. The final values of a, b, c, d, e, f, g, h from the large loop are concatenated to form the final 256-bit message digest.

The specific functions in the 64-step loop are as follows:

For t = 0 to 63
T1 = (h + (∑1(e) + CH(e, f, g) + Kt + Wt) mod 2^32
T2 = ∑0(a) + MAJ(a, b, c) mod 2^32
h = g
g = f
f = e
e = (d + T1) mod 2^32
d = c
c = b
b = a
a = (T1 + T2) mod 2^32

Where ∑1(e) and ∑0(a) are bitwise rotation functions of e and a, respectively; CH(e, f, g) is a function that combines e, f, and g; T1 and T2 are two temporary variables generated at each step; Kt is a randomly selected value from the random string; Wt is the processing function for the input message block.

The processing of the message block: Each message block is decomposed into 16 32-bit words, denoted as w[0], …, w[15]. This means that the first 16 words are directly obtained from the first i block of the message, while the remaining words are obtained through the following iterative formula:

Wt = w[t], 0 ≤ t < 16
Wt = σ1(Wt−2) + Wt−7 + σ0(Wt−15) + Wt−16, 16 ≤ t < 63

The eight variables generated in the last iteration are combined to form the hash string Hi corresponding to the i-th block, which is the hash value obtained from SHA256 encryption.

  1. Applications and Code Implementation of SHA256 in Blockchain

SHA256 has proven to be very secure since its inception, and it is widely used in blockchain, such as in the mining algorithm of Bitcoin, generating account addresses, and generating hashes for blocks in Ethereum, etc. Its use is very simple, as the SHA256 algorithm has already been encapsulated in the Go language library, allowing us to directly call the method. The function hash.Sha256 takes a byte slice as input and returns a byte slice as output, which is important to note when using it.

Go code implementation of SHA256 can be done in two ways, both of which yield the same result:

package main

import (
    "github.com/nebulasio/go-nebulas/crypto/hash"
    "fmt"
    "encoding/hex"
    "crypto/sha256"
)

func main() {

    a := "helloworld"

    // Method 1: Direct output using one method
    hash := hash.Sha256([]byte(a))
    fmt.Println(hex.EncodeToString(hash))

    // Method 2: Step-by-step output
    h := sha256.New() // Create SHA256 algorithm
    h.Write([]byte(a)) // Encrypt parameter a using SHA256, obtaining 8 variables
    hash1 := h.Sum(nil) // Sum the 8 variables to obtain the final hash

    fmt.Println(hex.EncodeToString(hash1))
}

// Output
// 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
// 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af

Digital Signatures#

Digital signatures play an important role in information security, including identity authentication, data integrity, non-repudiation, and anonymity. They are an important branch of modern cryptography. The signing process involves the sender using their private key to perform a so-called encryption operation on the message, resulting in a hash value, which is the signature. The signature and the message are sent to the recipient. The recipient uses the sender's public key and the received message to verify the signature, confirming that the received message is complete and accurate; otherwise, it indicates that the message source is incorrect.

In summary, digital signatures involve signing with a private key and verifying with a public key.

  1. Ordinary Signatures

A signature is simply made with a private key, and the signing action is performed by the sender themselves. Common signature methods include RSA digital signatures, DSS digital signatures, ElGamal digital signatures, ECDSA digital signatures, etc. Among these, RSA and ECDSA signatures have already been discussed in the encryption algorithms section. The most commonly used signature method in blockchain projects is the ECDSA digital signature. The signing and verification principles are not elaborated here; interested readers can refer to previous chapters.

Given the importance of ECDSA digital signatures in blockchain, I will now explain how to apply it. The following code demonstrates how to use ECDSA to sign data BLOCK before it can be added to the chain, requiring verification before proceeding.

The process includes:

  1. Generating a private key using ecdsa.GenerateKey;
  2. Generating a public key from the private key;
  3. Performing a hash operation on the BLOCK data, which is essentially the mining process in a public chain;
  4. Creating a custom signing method to allow for signing of data of any length;
  5. Verifying the data's legality, meaning that the public key must validate the signature before the data can be added to the chain.
package main

import (
    "bytes"
    "encoding/binary"
    "log"
    "time"
    "crypto/sha256"
    "fmt"
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "math/big"
)

// Simple blockchain block structure
type Block struct {
    // 1. Block height
    Height int64
    // 2. Previous block hash
    PrevBlockHash []byte
    // 3. Transaction data
    Data []byte
    // 4. Timestamp
    Timestamp int64
    // 5. Actual hash obtained through mining
    Hash []byte
    // 6. Random number Nonce
    Nonce int64
}

func main() {
    // Generate private key
    prk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

    // Generate public key
    pubkey := prk.PublicKey

    // Data to be added to the chain
    data := []byte("helloworld")

    // Manually create block information; in practice, this is triggered automatically through transactions
    block := &Block{2, nil, data, time.Now().Unix(), nil, 0}

    // Concatenate block information into a byte array
    blockbytes := prepareData(block)

    // Perform hash operation on the block, which is essentially the mining process
    blockHash := sha256.Sum256(blockbytes) // Sign
    signature, _ := Sign(blockHash[:], prk)

    // Verify; if successful, proceed to add to the chain; otherwise, rollback
    if Verify(blockHash[:], signature, &pubkey) {
        fmt.Println("This block is valid and can be added to the chain")
    } else {
        fmt.Println("This block is invalid, rollback")
    }
}

// Data concatenation
func prepareData(block *Block) []byte {
    Block := block
    data := bytes.Join(
        [][]byte{
            Block.PrevBlockHash,
            Block.Data,
            IntToHex(Block.Timestamp),
            Block.Data,
            IntToHex(int64(Block.Nonce)),
            IntToHex(int64(Block.Height)),
        },
        []byte{},
    )
    return data
}

// Convert int64 to byte array
func IntToHex(num int64) []byte {
    buff := new(bytes.Buffer)
    err := binary.Write(buff, binary.BigEndian, num)
    if err != nil {
        log.Panic(err)
    }

    return buff.Bytes()
}

func Sign(data []byte, privkey *ecdsa.PrivateKey) ([]byte, error) {
    // Hash the message to be signed, generating a 32-byte length byte array
    digest := sha256.Sum256(data)

    // Sign the hashed plaintext using elliptic curve method, returning two big.Int type large numbers
    r, s, err := ecdsa.Sign(rand.Reader, privkey, digest[:])
    if err != nil {
        return nil, err
    }
    // Convert the large numbers to byte arrays and concatenate them to form the signature
    signature := append(r.Bytes(), s.Bytes()...)
    return signature, nil
}

// Verify the signature using the public key
func Verify(data, signature []byte, pubkey *ecdsa.PublicKey) bool {
    // Convert the plaintext to a byte array
    digest := sha256.Sum256(data)

    // Declare two big integers r and s
    r := big.Int{}
    s := big.Int{}
    // Split the signature into two parts and convert the slices to *big.Int types
    sigLen := len(signature)
    r.SetBytes(signature[:(sigLen / 2)])
    s.SetBytes(signature[(sigLen / 2):])

    // Verify using the public key
    return ecdsa.Verify(pubkey, digest[:], &r, &s)
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.