BTC
基础
Block chain is a linked list using hash pointers.
哈希
- collision resistance
- hiding
- puzzle friendly
签名
asymmetric encryption algorithm
sign with private key, verify with public key
哈希指针
- block chain, tamper-evident log
- merkle tree
- block header+block body
- 全节点、轻节点
协议
- double spending attack
- coinbase tx
- consensus
- longest valid chain
- block reward
- human readable difficulty:
difficulty = difficulty_1_target / current_target - nbits
实现
- transaction-based ledger
- UTXO
- Bernoulli trail
网络
- appplication layer: BitCoin Block Chain
- network layer: P2P
分叉
- state fork
- forking attack
- deliberate fork
- protocol fork
- hard fork
- soft fork
数据结构
一笔尚未被打包进区块、但已完全序列化好的 比特币原始交易:
{
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
"vout": 0,
"scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.015,
"scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
},
{
"value": 0.0845,
"scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG"
}
]
}
一些要注意的细节
- 每个In必须都要Out。在不考虑Gas的情况下,A->B 100sato,如果只有50和60两个out指向A(即A有110 sato),那么也必须花出110 sato,即A->B 100sato,A->A 10 sato。
- 为什么同一笔转账需求(比如给 A 付 1 BTC、给 B 付 2 BTC)不直接写在一条交易的多个输出里,而要把这些输出拆到多条独立的交易去发?
要么因为签名主体不同、要么因为时间锁/体积/隐私/协议限制,导致它们无法或不适合放进同一笔交易的多个输出里。比特币本身完全可以这样做。 - 每次执行脚本,是将
input script与前一次交易的output script拼接执行。如果有多个输入,每个输入都要和对应输出的脚本匹配并验证执行。 scriptSig:也叫input script,解锁脚本,放在vin中,证明我有权花这笔钱。scriptPubKey:也叫output script,锁定脚本,放在vout中,指定谁有权花这笔钱。
传统P2PKH(Pay-to-Public-Key-Hash)格式:
大致流程:"scriptSig": <签名> <公钥> "scriptPubKey": OP_DUP OP_HASH160 <20 bytes 公钥哈希> OP_EQUALVERIFY OP_CHECKSIGPUSHDATA往栈中压入签名和公钥,复制一份公钥做哈希并与给出的PublicKeyHash比对是否一致,确认公钥正确后再用公钥检查签名。
签名是指对本次交易简化哈希做一次 ECDSA 私钥签名,具体有点复杂。DUP:复制栈中元素 HASH160:栈中元素弹出取哈希,并将哈希压入栈 EQUALVERIFY:比较是否相等
原始P2PK(Pay-to-Public-Key)格式:
还有P2SH格式等,"scriptSig": <签名> "scriptPubKey": <公钥> OP_CHECKSIG
其中的"scriptSig": <签名><serialized redeemScript> "scriptPubKey": OP_HASH160 <20 B redeemScript哈希> OP_EQUALredeemScript(赎回脚本)可以是P2PK/P2PKH/多重签名 等各种格式。- 相比于P2PK,P2PKH能够一定程度上保护你的公钥,只要你不花这笔钱,链上出现的就只有你的公钥哈希值。
实现
javascript
generated by Claude Code
const crypto = require('crypto');
class Block {
constructor(timestamp, transactions, previousHash = '') {
this.timestamp = timestamp;
this.transactions = transactions;
this.previousHash = previousHash;
this.nonce = 0;
this.hash = this.calculateHash();
}
calculateHash() {
return crypto
.createHash('sha256')
.update(this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce)
.digest('hex');
}
mineBlock(difficulty) {
const target = Array(difficulty + 1).join('0');
while (this.hash.substring(0, difficulty) !== target) {
this.nonce++;
this.hash = this.calculateHash();
}
console.log(`Block mined: ${this.hash}`);
}
}
class Transaction {
constructor(fromAddress, toAddress, amount) {
this.fromAddress = fromAddress;
this.toAddress = toAddress;
this.amount = amount;
this.timestamp = Date.now();
}
}
class Blockchain {
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 2;
this.pendingTransactions = [];
this.miningReward = 100;
}
createGenesisBlock() {
return new Block(Date.parse('2024-01-01'), [], '0');
}
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
minePendingTransactions(miningRewardAddress) {
const rewardTransaction = new Transaction(null, miningRewardAddress, this.miningReward);
this.pendingTransactions.push(rewardTransaction);
let block = new Block(Date.now(), this.pendingTransactions, this.getLatestBlock().hash);
block.mineBlock(this.difficulty);
console.log('Block successfully mined!');
this.chain.push(block);
this.pendingTransactions = [];
}
createTransaction(transaction) {
if (!transaction.fromAddress || !transaction.toAddress) {
throw new Error('Transaction must include from and to address');
}
if (!this.isValidTransaction(transaction)) {
throw new Error('Invalid transaction: insufficient balance');
}
this.pendingTransactions.push(transaction);
}
getBalance(address) {
let balance = 0;
for (const block of this.chain) {
for (const trans of block.transactions) {
if (trans.fromAddress === address) {
balance -= trans.amount;
}
if (trans.toAddress === address) {
balance += trans.amount;
}
}
}
return balance;
}
isValidTransaction(transaction) {
if (transaction.fromAddress === null) return true;
return this.getBalance(transaction.fromAddress) >= transaction.amount;
}
getAllTransactionsForWallet(address) {
const txs = [];
for (const block of this.chain) {
for (const tx of block.transactions) {
if (tx.fromAddress === address || tx.toAddress === address) {
txs.push(tx);
}
}
}
return txs;
}
isChainValid() {
const realGenesis = JSON.stringify(this.createGenesisBlock());
if (realGenesis !== JSON.stringify(this.chain[0])) {
return false;
}
for (let i = 1; i < this.chain.length; i++) {
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
if (!currentBlock.hasValidTransactions()) {
return false;
}
if (currentBlock.hash !== currentBlock.calculateHash()) {
return false;
}
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
}
return true;
}
}
Block.prototype.hasValidTransactions = function() {
for (const tx of this.transactions) {
if (!tx.fromAddress || !tx.toAddress) continue;
}
return true;
};
module.exports = { Blockchain, Transaction };
const crypto = require('crypto');
const { Transaction } = require('./basic-btc-blockchain');
class Wallet {
constructor() {
this.keyPair = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
this.publicKey = this.keyPair.publicKey;
this.privateKey = this.keyPair.privateKey;
}
getAddress() {
return crypto.createHash('sha256')
.update(this.publicKey)
.digest('hex')
.substring(0, 40);
}
signTransaction(transaction) {
if (transaction.fromAddress !== this.getAddress()) {
throw new Error('You cannot sign transactions for other wallets!');
}
const hashTx = crypto.createHash('sha256')
.update(JSON.stringify(transaction))
.digest('hex');
const sign = crypto.createSign('SHA256');
sign.update(hashTx);
sign.end();
return sign.sign(this.privateKey, 'hex');
}
createTransaction(toAddress, amount, blockchain) {
if (amount <= 0) {
throw new Error('Transaction amount must be greater than 0');
}
if (blockchain.getBalance(this.getAddress()) < amount) {
throw new Error('Insufficient balance');
}
const transaction = new Transaction(this.getAddress(), toAddress, amount);
transaction.signature = this.signTransaction(transaction);
return transaction;
}
static verifyTransaction(transaction) {
if (transaction.fromAddress === null) return true;
if (!transaction.signature || transaction.signature.length === 0) {
throw new Error('No signature in this transaction');
}
const publicKey = crypto.createPublicKey({
key: Buffer.from(`-----BEGIN PUBLIC KEY-----\n${transaction.fromAddress}\n-----END PUBLIC KEY-----`, 'utf8'),
format: 'pem'
});
const hashTx = crypto.createHash('sha256')
.update(JSON.stringify({
fromAddress: transaction.fromAddress,
toAddress: transaction.toAddress,
amount: transaction.amount,
timestamp: transaction.timestamp
}))
.digest('hex');
const verify = crypto.createVerify('SHA256');
verify.update(hashTx);
verify.end();
return verify.verify(publicKey, transaction.signature, 'hex');
}
}
module.exports = Wallet;
const { Blockchain, Transaction } = require('./basic-btc-blockchain');
const Wallet = require('./wallet');
console.log('🚀 Starting Bitcoin Blockchain Demo...\n');
const myCoin = new Blockchain();
const wallet1 = new Wallet();
const wallet2 = new Wallet();
console.log(`👛 Wallet 1 Address: ${wallet1.getAddress()}`);
console.log(`👛 Wallet 2 Address: ${wallet2.getAddress()}`);
console.log('\n⛏️ Mining first block...');
myCoin.minePendingTransactions(wallet1.getAddress());
console.log(`\n💰 Balance of Wallet 1: ${myCoin.getBalance(wallet1.getAddress())}`);
console.log(`💰 Balance of Wallet 2: ${myCoin.getBalance(wallet2.getAddress())}`);
console.log('\n💸 Creating transaction from Wallet 1 to Wallet 2...');
try {
const tx1 = wallet1.createTransaction(wallet2.getAddress(), 50, myCoin);
myCoin.createTransaction(tx1);
console.log('✅ Transaction created successfully!');
} catch (error) {
console.log('❌ Error creating transaction:', error.message);
}
console.log('\n⛏️ Mining pending transactions...');
myCoin.minePendingTransactions(wallet2.getAddress());
console.log(`\n💰 Balance of Wallet 1: ${myCoin.getBalance(wallet1.getAddress())}`);
console.log(`💰 Balance of Wallet 2: ${myCoin.getBalance(wallet2.getAddress())}`);
console.log('\n📊 Transaction history for Wallet 1:');
const wallet1Transactions = myCoin.getAllTransactionsForWallet(wallet1.getAddress());
wallet1Transactions.forEach((tx, index) => {
console.log(` ${index + 1}. ${tx.fromAddress ? 'Sent' : 'Received'} ${tx.amount} BTC to ${tx.toAddress}`);
});
console.log('\n🔗 Blockchain validation...');
console.log(`Is blockchain valid? ${myCoin.isChainValid() ? '✅ Yes' : '❌ No'}`);
console.log('\n📋 Complete blockchain:');
myCoin.chain.forEach((block, index) => {
console.log(`\nBlock ${index}:`);
console.log(` Hash: ${block.hash}`);
console.log(` Previous Hash: ${block.previousHash}`);
console.log(` Timestamp: ${new Date(block.timestamp).toLocaleString()}`);
console.log(` Transactions: ${block.transactions.length}`);
console.log(` Nonce: ${block.nonce}`);
});
console.log('\n🎯 Demo completed!');
java
ETH
特点
- mining puzzle: memory hard, ASIC resistance
- pow->pos
- smart contract
- account-based ledger
vulnerable to replay attack
为什么BTC不会出现重放攻击:因为有UTXO
ETH的解决办法:添加一个账户总交易次数(nonce)的字段 - transaction-driven state machine
账户
- externally owned account
- balance
- nonce(交易次数)
- smart contract account
- balance
- nonce(创建过的合约数量)
- code
- storage
- 所有交易只能由外部账户发起,然后去调用合约账户,合约账户可以调用其它合约账户
数据结构
https://github.com/ethereum/go-ethereum
https://learnblockchain.cn/2018/01/04/understanding-smart-contracts
以太坊 Merkle Patricia Tree 全解析 - 知乎
状态树
tx-based ledger中,通过merkle tree存储所有的交易,就可以变相存储每个地址的余额。account-based ledger中,如何让每个节点都能确认每个账户的余额?
- 哈希表❌快速查找,但无法确认有效性、哈希碰撞
- 默克尔树❌插入消耗太大、每个节点配对方式不一、空间太大、动态数据不友好。。。
- MPT(Merkle Patricia Trie)✅
- 分支数(branching factor)取决于key范围,eth中为17。
- 查找效率取决于key长,eth中长度固定40
- 不会碰撞
- 结构与插入顺序无关
- 更新友好,不用遍历别的树
- 压缩适合稀疏的地址(Patricia)
- 不可篡改(Merkle)
- Modified MPT✅
- 压缩节点Extension Node:
- Branch Node:
- Leaf Node:
每次打包区块时,只要改动对应的一部分节点,通过“旧 root 哈希”仍能索引到整棵旧树,以便快速回滚。
value使用RLP序列化再存储。

MPT的Merkle Proof
以上图为例,我想证明叶节点{'a77d397':0.12ETH}存在,Merkle Proof只需要提供路径上的所有经过的中间节点,验证者收到后要做几件事:
- 计算叶节点
{'a77d397':0.12ETH}的哈希H(leaf1) - 比较
H(leaf1)是否与上一Branch Node的槽9中值一致 - 计算分支节点的哈希
H(branch1) - 比较
H(branch1)是否与上一个扩展节点的next node槽中的值一致 - 计算扩展节点的哈希
H(ex1) - 以此类推......
- 计算根节点哈希并与自己手上的根哈希比较,最后验证了该账户存在。
交易树和收据树
交易树只证明“发生了什么”,收据树才证明“结果是什么”。
轻节点想要查询某个交易/某种交易,需要使用布隆过滤器。
先查询块头的Bloom Filter,把确定没有的块排除,再查询每个块的收据树的Bloom Filter.
交易树没有Bloom 结构
// NewBlock creates a new block. The input data is copied, changes to header and to the
// field values will not affect the block.
//
// The body elements and the receipts are used to recompute and overwrite the
// relevant portions of the header.
//
// The receipt's bloom must already calculated for the block's bloom to be
// correctly calculated.
func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher) *Block {
if body == nil {
body = &Body{}
}
var (
b = NewBlockWithHeader(header)
txs = body.Transactions
uncles = body.Uncles
withdrawals = body.Withdrawals
)
// 创建交易树 交易列表
// 如果没有交易就返回空哈希
if len(txs) == 0 {
b.header.TxHash = EmptyTxsHash
} else {
// 交易树的根哈希
b.header.TxHash = DeriveSha(Transactions(txs), hasher)
// 创建交易列表
b.transactions = make(Transactions, len(txs))
copy(b.transactions, txs)
}
// 创建收据树
if len(receipts) == 0 {
b.header.ReceiptHash = EmptyReceiptsHash
} else {
b.header.ReceiptHash = DeriveSha(Receipts(receipts), hasher)
// Receipts must go through MakeReceipt to calculate the receipt's bloom
// already. Merge the receipt's bloom together instead of recalculating
// everything.
// 创建块头的BloomFilter
b.header.Bloom = MergeBloom(receipts)
}
// 构建叔父区块
if len(uncles) == 0 {
b.header.UncleHash = EmptyUncleHash
} else {
b.header.UncleHash = CalcUncleHash(uncles)
b.uncles = make([]*Header, len(uncles))
for i := range uncles {
b.uncles[i] = CopyHeader(uncles[i])
}
}
if withdrawals == nil {
b.header.WithdrawalsHash = nil
} else if len(withdrawals) == 0 {
b.header.WithdrawalsHash = &EmptyWithdrawalsHash
b.withdrawals = Withdrawals{}
} else {
hash := DeriveSha(Withdrawals(withdrawals), hasher)
b.header.WithdrawalsHash = &hash
b.withdrawals = slices.Clone(withdrawals)
}
return b
}
共识机制:Ghost Protocol
ETH出块速度太快,分支太多,mining centralization影响严重。
GHOST v1:非最长链的2个前区块为叔父区块,能获得7/8的奖励。子区块记录叔父区块也能获得1/32的奖励。
- 只能有两个叔父,第三个开始没有理由放弃
- 时间差导致完成前能看见的叔父不为2
- 自私矿工可能故意不包含叔父
改进:前6级区块都可以被认定为叔父,奖励逐级递减。
叔父区块只检查header。
算法
目标:防止挖矿设备专业化ASIC resistance,所以要memory hard,同时也要easy to verify。
Ethash :改进版的scrypt。
16M cache+1G DAG.
难度调整
权益证明
也叫Virtual Mining。
POS才是内循环的,不会被外部资源所影响。但是需要proof of deposit.
如何从POW转向POS?社区希望在不抛弃 PoW 的前提下,引入 PoS 的“最终性”机制,为后续全面 PoS(信标链)做演练。于是提出 Casper FFG——一个“附加在 PoW 上的最终性小工具(gadget)”
Casper the Friendly Finality Gadget(FFG)
流程
- 验证者(Validator) 需链上抵押 32 ETH,系统随机选出他们组成一个“投票委员会”。
- 每 100 个 PoW 区块组成一个 epoch,验证者两轮投票(two-phase commit):
- prepare message(预投票) → 2/3 以上权重同意某个检查点;
- commit message(提交票) → 再一轮 2/3 以上确认。
- 当同一检查点连续两轮获得 2/3 质押权重时,该检查点及之前所有区块被最终确定(finalized)。
此时要回滚就必须至少 1/3 的质押 ETH 被销毁,经济代价远高于 PoW 的算力重组,由此实现“经济最终性” - 实际协议上,每50个区块分为一个epoch,只进行一轮投票,每次投票对于前一个epoch是commit message,对于后一个epoch是prepare message。
奖惩
- 奖励:在线且正确投票的验证者按质押比例分得增发的 ETH。
- Slashing(削减):同一人重复投票(两边下注)或双重提议(同一秒内出两个块),链会没收其部分乃至全部押金(最低 0.5 ETH,最高全额 32 ETH)。
- 怠工惩罚:长期不在线、导致两轮投票达不到 2/3,系统也会微量扣款,迫使验证者保持活跃。
- 如果有至少1/3的验证者重复投票,finality也会被推翻,这些验证者的保证金也会被没收。