需求分析可以支持多条链，只要链支持Solidity智能合约即可可以支持多个代币，需要符合ERC20标准的代币由去中心化的中继节点采用投票方式来完成跨链请求的确认每个中继节点需要质押保证金，每成功一笔跨链交易可获取奖励引入惩罚机制，中继节点每次作恶都会被罚没，保证金低于一定值时被封禁，重新充入保证金可解封引入黑名单管理机制，符合监管需求，禁止名单内账户使用跨链桥功能引入中继节点准入机制，维持中继节点集群稳定，保障跨链桥的稳定工作跨链交易规则跨链有最低金额限制（比如100USDT）每笔收取总金额的一定比例作为手续费（比如万分之五）一定比例的手续费平分给参与投票的中继节点（比如70%）剩下比例的手续费发给平台（比如30%）用户需手动去领取跨链资产，也可以在指定一个帮忙自动提取的中继节点（需额外付费）中继节点规则中继节点列表由平台进行管理维护，需保持稳定（避免在短时间内大量增加或减少）保证金由中继节点自行维护，存入自由，取出需要限制（比如每隔24小时可取一次）中继节点数量需保持一定合适的数量，至少也得3个，太多中继节点也不好（浪费机器资源，也拖慢投票速度）投票罚没规则中继节点需保证金大于指定门槛的才可以参与投票（比如10万），低于的处于封禁状态中继节点的投票权重都一样，和保证金大小无关每次跨链请求需要大于2/3的中继节点（所有的包括封禁的）投票才算通过投票时需要附带中继节点的签名（合约可以验证签名来核查投票结果并允许用户提取对应资产）每次作恶罚没一定比例的保证金（比如5万），罚没的保证金转给平台地址如果某个节点持续作恶，会不断损失保证金，平台也可以将其从中继节点列表中移除跨链通道要进行跨链交易，需要在两条链之间创建一个通道通道是有方向的，一般情况要添加双向的：A链->B链和B链->A链通道需指定两条链关联的代币地址相同的两条链可添加多对代币地址参与角色普通用户-web前端：发起跨链请求、领取跨链资产、查看交易记录（历史的和进行中的）中继节点-后台服务：跨链交易投票、帮用户自动领取跨链资产中继节点-web前端：查看管理保证金、查看收益、查看投票记录平台管理-web前端：查看管理中继节点、查看保证金、查看收益列表、查看修改黑名单、查看交易记录数据分析与监控：交易汇总分析、中继节点收益分析、资金流动分析、跨链桥异常预警合约设计核心流程：用户在A链存入（冻结）资产、中继节点投票（签名）确认、使用超2/3的签名在B链取出（解冻）资产引入一个廉价的链来存储投票过程，而不是中继节点自行共识，投票过程更安全，也可追溯，提高系统的可用性、安全性用来投票的链这里称为桥链（bridge chain），它还可用来存储一些其它数据或运行一些服务使用签名技术减少合约调用次数，简化设计与节省交易gas费为了节省交易gas费，所有投票在桥链上完成，其它功能也尽量在桥链上实现投票就是签名，用户投票则是将签名写入到桥链合约中跨链通道定义：通道id=keccak256(来源链id,来源链代币地址,目标链id,目标链代币地址)跨链交易定义：交易id=keccak256(通道id,时间戳,接收者,金额,手续费)跨链交易记为KR，投票就是对KR进行签名，记为：KS，有效投票需满足数据无篡改和签名可验证架构桥核心合约（资产）：跨链资金出入口、中继成员列表、黑名单列表桥治理合约（投票）：签名存储与验证、保证金管理、惩罚机制、手续费分发桥链部署：治理合约A链部署：资产合约B链部署：资产合约C链部署：资产合约桥核心合约接口-添加中继节点接口-移除中继节点接口-将账户地址加入黑名单接口-将账户地址从黑名单删除接口-添加跨链通道接口-移除跨链通道接口-跨链转入资产（用户资产充足且未在黑名单内）接口-跨链取出资产（需通过2/3的中继节点签名验证）事件-中继节点变更、黑名单变更、资产转入、资产转出、资产解冻、交易取消查询-中继节点、黑名单、跨链USDT额度（到不同链上的额度）变量-跨链USDT额度列表、中继节点列表、黑名单列表桥治理合约接口-增加保证金接口-减少保证金接口-上传签名（验证签名，作恶罚没）事件-保证金变更、签名提交、投票结果、罚没查询-保证金、签名、罚没次数、罚没金额变量-保证金余额列表、签名列表、跨链交易列表、惩罚记录表合约执行以以太坊和波场为例，以稳定币USDT为例初始化两条链的桥核心合约设置相同的10个中继节点地址和黑名单列表在桥链的桥治理合约中为10个中继节点地址冲入20万保证金平台往两条链的桥核心合约中存入10万USDT作为流动资金Ethereum >>> Tron 跨链用户调用Ethereum链资产合约的跨链转入接口检查用户不在黑名单内，黑名单用户交易直接失败将用户的1万个USDT转到资产合约地址名下更新变量：到Tron链的跨链USDT额度减少1万发出事件：跨链转入（起始链ID + 目标链ID + 金额 + 目标链代币合约地址）中继节点调用桥链上的桥治理合约上传签名接口接收用户的跨链请求事件，可以等待N个区块确认（避免区块分叉导致交易失效）对此跨链事件进行签名并提交到投票合约中（如果不认可则可以忽略）在签名满7个时检查投票结果，如果有超2/3的有效票则进行以下操作：对此次跨链请求作恶的中继节点执行罚没分发跨链交易手续费发出事件：投票完成（跨链请求交易哈希 + 可提取代币地址 + 可提取金额 + 7个签名）中继节点调用Tron链桥核心合约的跨链转出接口检查合约持有的USDT是否足够检查7个签名是否有效，且签名地址在中继节点列表中给用户指定的接收地址转9995个USDT（从桥核心合约地址名下）更新变量：Ethereum链的跨链USDT额度增加1万发出事件：跨链取出（跨链请求交易哈希 + 代币地址 + 金额）状态变更总结Ethereum链的资产合约:持有的USDT数量：10万 -> 11万桥链的投票合约：签名列表：+ 跨链请求的7个有效签名Tron链的资产合约：持有的USDT数量：10万 -> 9万用户在Ethereum上的USDT减少1万，在Tron链上获得9995个7个中继节点在桥链上获得3.5/7个USDT平台获得1.5个USDTTron >>> Ethereum 跨链此操作和Ethereum >>> Tron跨链操作流程一样跨链手续费同样在桥治理合约中分发一开始只有两条链上的USDT很少，后期会随着交易量增长用户兑换时可检查目标链上是否有充足的币来决定是否发起交易如果用户一时提取不到资产，待桥核心合约有足够的资金再重试跨链桥特点同一笔跨链交易需按顺序执行：A链转入、投票、B链转出需要2/3的中继节点投票，确保每一笔交易都是有效的不同跨链交易可异步执行，桥链的TPS高，速度快使用相对廉价的链合约来处理投票过程，安全性高，透明度高惩罚机制，提高相同的稳定性多链多币支持，拓展性强平台只是审查中继节点列表，跨链交易由中继节点共识投票确认，去中心化程度高
与以太坊的账户模型不同，比特币是没有余额概念的，使用的是UTXO模型。所以在比特币网络中，存储比特币的是交易输出，准确点说就是未使用过的交易输出。UTXOUTXO是Unspent Transaction Outputs的缩写（Transaction可简写为TX），即未使用过的交易输出。比特币节点会追踪UTXO集合，从而确定哪些代币未被花费，以及哪些人可以花费它们。最初的比特币是通过铸币交易创造的，它是用来奖励矿工的一种特殊交易，因为是凭空创造的，所以它没有输入的。新的UTXO是通过花费现有UTXO创建的，UTXO包含了接收地址和金额。每个比特币交易都由输入和输出组成。输入花费已有UTXO，输出则创建新的UTXO，UTXO只要被用一次，就不存在“未花费”了，以后就不会再被引用，这样就解决双花（双重支付）问题。当我们要使用一个UTXO时，需要对UTXO进行解锁（一般是用私钥对UTXO进行签名），虽然UTXO是公开的，但是如果没有私钥去解锁，则不能花费UTXO。交易结构交易是比特币的核心数据结构，包括区块在内的数据结构都是在为交易服务。交易主要由未花费的输入和输出组成，输出总金额不能大于输入的总金额，多余的部分为交易矿工费有三种类型交易铸币交易（Generation TX），有额外的Coinbase字段，矿工可以随意填写一些信息，因为是挖矿所得，所以这种交易类型没有输入，输出数量就是矿工所得，地址是矿工的地址。普通交易（Pubkey Hash TX），转账交易，一个或多个输入，一个或两个输出，第一个输出称为支付，第二个输出称为找零脚本交易（Script Hash TX），一般为多签脚本，为普通交易升级版本，可以生成灵活的锁定脚本，然后使用特定的解锁脚本去花费比特币字段表数据项大小(Byte)数据类型描述nVersion4int32_t交易版本vin不定CTxIn列表交易输入vout不定CTxOut列表交易输出nLockTime4uint32_t交易锁定时间交易输入CTxIn数据项大小(Byte)数据类型描述prevout不定COutPoint上一个交易的输出，由交易哈希和索引值确定scriptSig不定char[]解锁脚本nSequence4uint32_t序列号，可用于相对时间锁定交易输出CTxOut数据项大小(Byte)数据类型描述nValue8int64_t交易输出金额，单位为SatoshisscriptPubKey不定char[]锁定脚本，定义花费须满足的条件创币交易CoinbaseTransaction每一个区块内包含的第一条交易为CoinbaseTransaction，它作为对挖出该区块的矿工的比特币奖励交易。没有交易输入交易哈希为0输出的索引值固定，为0xffffffffBIP34规定增加一个4字节的height的字段，这个参数是必须的源码参考代码路径：https://github.com/bitcoin/bitcoin/blob/master/src/primitives/transaction.hCOutPut/** An outpoint - a combination of a transaction hash and an index n into its vout * ** 一个交易哈希值与输出下标的集合 */ class COutPoint { public: uint256 hash; //交易哈西 uint32_t n; //对应序列号 COutPoint(): n((uint32_t) -1) { } COutPoint(const uint256& hashIn, uint32_t nIn): hash(hashIn), n(nIn) { } ADD_SERIALIZE_METHODS; //用来序列化数据结构，方便存储和传输 template <typename Stream, typename Operation> inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(hash); READWRITE(n); } void SetNull() { hash.SetNull(); n = (uint32_t) -1; } bool IsNull() const { return (hash.IsNull() && n == (uint32_t) -1); } //小于号<重载函数 friend bool operator<(const COutPoint& a, const COutPoint& b) { int cmp = a.hash.Compare(b.hash); return cmp < 0 || (cmp == 0 && a.n < b.n); } //==重载函数 friend bool operator==(const COutPoint& a, const COutPoint& b) { return (a.hash == b.hash && a.n == b.n); } //!=重载函数 friend bool operator!=(const COutPoint& a, const COutPoint& b) { return !(a == b); } std::string ToString() const; };CTxIn/** An input of a transaction. It contains the location of the previous * transaction's output that it claims and a signature that matches the * output's public key. * **交易的输入，包括当前输入所对应上一笔交易的输出位置， *并且还包括上一笔输出所需要的签名脚本 */ class CTxIn { public: COutPoint prevout; //上一笔交易输出位置 CScript scriptSig; //解锁脚本 uint32_t nSequence; /**序列号，可用于交易的锁定 nSequence字段的设计初心是想让交易能在在内存中修改，可惜后面从未运用过 对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易， nSequence值必须设置为小于2^32，以使时间锁定器有效。通常设置为2^32-1 由于BIP-68的激活，新的共识规则适用于任何包含nSequence值小于2^31的输入的交易（bit 1<<31 is not set）。 以编程方式，这意味着如果没有设置最高有效（bit 1<<31），它是一个表示“相对锁定时间”的标志。 否则（bit 1<<31set），nSequence值被保留用于其他用途， 例如启用CHECKLOCKTIMEVERIFY，nLocktime，Opt-In-Replace-By-Fee以及其他未来的新产品。 一笔输入交易，当输入脚本中的nSequence值小于2^31时，就是相对时间锁定的输入交易。 这种交易只有到了相对锁定时间后才生效。例如， 具有30个区块的nSequence相对时间锁的一个输入的交易 只有在从输入中引用的UTXO开始的时间起至少有30个块时才有效。 由于nSequence是每个输入字段，因此交易可能包含任何数量的时间锁定输入， 所有这些都必须具有足够的时间以使交易有效。 */ CScriptWitness scriptWitness; //! Only serialized through CTransaction /* Setting nSequence to this value for every input in a transaction * disables nLockTime. * * 规则1:如果一笔交易中所有的SEQUENCE_FINAL都被赋值了相应的nSequence，那么nLockTime就会被禁用 */ static const uint32_t SEQUENCE_FINAL = 0xffffffff; /* Below flags apply in the context of BIP 68*/ /* If this flag set, CTxIn::nSequence is NOT interpreted as a * relative lock-time. * * 规则2:如果设置了该值，nSequence不被用于相对时间锁定。规则1失效 */ static const uint32_t SEQUENCE_LOCKTIME_DISABLE_FLAG = (1 << 31); /* If CTxIn::nSequence encodes a relative lock-time and this flag * is set, the relative lock-time has units of 512 seconds, * otherwise it specifies blocks with a granularity of 1. * * 规则3：如果规则1有效并且设置了此变量，那么相对锁定时间单位为512秒，否则锁定时间就为1个区块 */ static const uint32_t SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22); /* If CTxIn::nSequence encodes a relative lock-time, this mask is * applied to extract that lock-time from the sequence field. * * 规则4：如果nSequence用于相对时间锁，即规则1有效，那么这个变量就用来从nSequence计算对应的锁定时间 */ static const uint32_t SEQUENCE_LOCKTIME_MASK = 0x0000ffff; /* In order to use the same number of bits to encode roughly the * same wall-clock duration, and because blocks are naturally * limited to occur every 600s on average, the minimum granularity * for time-based relative lock-time is fixed at 512 seconds. * Converting from CTxIn::nSequence to seconds is performed by * multiplying by 512 = 2^9, or equivalently shifting up by * 9 bits. * * 相对时间锁粒度 * 为了使用相同的位数来粗略地编码相同的挂钟时间， * 因为区块的产生限制于每600s产生一个， * 相对时间锁定的最小单位为512是，512 = 2^9 * 所以相对时间锁定的时间转化为相当于当前值左移9位 */ static const int SEQUENCE_LOCKTIME_GRANULARITY = 9; CTxIn() { nSequence = SEQUENCE_FINAL; } explicit CTxIn(COutPoint prevoutIn, CScript scriptSigIn=CScript(), uint32_t nSequenceIn=SEQUENCE_FINAL); CTxIn(uint256 hashPrevTx, uint32_t nOut, CScript scriptSigIn=CScript(), uint32_t nSequenceIn=SEQUENCE_FINAL); ADD_SERIALIZE_METHODS; template <typename Stream, typename Operation> inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(prevout); READWRITE(scriptSig); READWRITE(nSequence); } friend bool operator==(const CTxIn& a, const CTxIn& b) { return (a.prevout == b.prevout && a.scriptSig == b.scriptSig && a.nSequence == b.nSequence); } friend bool operator!=(const CTxIn& a, const CTxIn& b) { return !(a == b); } std::string ToString() const; };CTxOut/** An output of a transaction. It contains the public key that the next input * must be able to sign with to claim it. * **交易输出，包含输出金额和锁定脚本 */ class CTxOut { public: CAmount nValue; //输出金额 CScript scriptPubKey; //锁定脚本 CTxOut() { SetNull(); } CTxOut(const CAmount& nValueIn, CScript scriptPubKeyIn); ADD_SERIALIZE_METHODS; template <typename Stream, typename Operation> inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(nValue); READWRITE(scriptPubKey); } void SetNull() { nValue = -1; scriptPubKey.clear(); } bool IsNull() const { return (nValue == -1); } friend bool operator==(const CTxOut& a, const CTxOut& b) { return (a.nValue == b.nValue && a.scriptPubKey == b.scriptPubKey); } friend bool operator!=(const CTxOut& a, const CTxOut& b) { return !(a == b); } std::string ToString() const; };CTransaction/** The basic transaction that is broadcasted on the network and contained in * blocks. A transaction can contain multiple inputs and outputs. * * ** 基本的交易，就是那些在网络中广播并被最终打包到区块中的数据结构。 * 一个交易可以包含多个交易输入和输出 */ class CTransaction { public: // Default transaction version. static const int32_t CURRENT_VERSION=2; //默认交易版本 // Changing the default transaction version requires a two step process: first // adapting relay policy by bumping MAX_STANDARD_VERSION, and then later date // bumping the default CURRENT_VERSION at which point both CURRENT_VERSION and // MAX_STANDARD_VERSION will be equal. /** 更改默认交易版本需要两个步骤： * 1.首先通过碰撞MAX_STANDARD_VERSION来调整中继策略， * 2.然后在稍后的日期碰撞默认的CURRENT_VERSION * * 最终MAX_STANDARD_VERSION和CURRENT_VERSION会一致 */ static const int32_t MAX_STANDARD_VERSION=2; // The local variables are made const to prevent unintended modification // without updating the cached hash value. However, CTransaction is not // actually immutable; deserialization and assignment are implemented, // and bypass the constness. This is safe, as they update the entire // strcture, including the hash. /** 下面这些变量都被定义为常量类型，从而避免无意识的修改了交易而没有更新缓存的hash值； * 然而CTransaction不是可变的 * 反序列化和分配被执行的时候会绕过常量 * 这才是安全的，因为更新整个结构包括哈希值 */ const std::vector<CTxIn> vin; //交易输入 const std::vector<CTxOut> vout; //交易输出 const int32_t nVersion; //版本 const uint32_t nLockTime; //锁定时间 private: /** Memory only. */ const uint256 hash; uint256 ComputeHash() const; public: /** Construct a CTransaction that qualifies as IsNull() */ CTransaction(); /** Convert a CMutableTransaction into a CTransaction. */ /**可变交易转换为交易*/ CTransaction(const CMutableTransaction &tx); CTransaction(CMutableTransaction &&tx); template <typename Stream> inline void Serialize(Stream& s) const { SerializeTransaction(*this, s); } /** This deserializing constructor is provided instead of an Unserialize method. * Unserialize is not possible, since it would require overwriting const fields. * ** 提供此反序列化构造函数而不是Unserialize方法。 * 反序列化是不可能的，因为它需要覆盖const字段 */ template <typename Stream> CTransaction(deserialize_type, Stream& s) : CTransaction(CMutableTransaction(deserialize, s)) {} bool IsNull() const { return vin.empty() && vout.empty(); } const uint256& GetHash() const { return hash; } // Compute a hash that includes both transaction and witness data uint256 GetWitnessHash() const; //计算包含交易和witness数据的散列 // Return sum of txouts. CAmount GetValueOut() const; //返回交易出书金额总和 // GetValueIn() is a method on CCoinsViewCache, because // inputs must be known to compute value in. /** * Get the total transaction size in bytes, including witness data. * "Total Size" defined in BIP141 and BIP144. * @return Total transaction size in bytes */ unsigned int GetTotalSize() const; // 返回交易大小 bool IsCoinBase() const //判断是否是创币交易 { return (vin.size() == 1 && vin[0].prevout.IsNull()); } friend bool operator==(const CTransaction& a, const CTransaction& b) { return a.hash == b.hash; } friend bool operator!=(const CTransaction& a, const CTransaction& b) { return a.hash != b.hash; } std::string ToString() const; bool HasWitness() const { for (size_t i = 0; i < vin.size(); i++) { if (!vin[i].scriptWitness.IsNull()) { return true; } } return false; } };
公钥：e、n，私钥：d、n，加密：C=M^e % n，解密：M=C^d % n。其中：n=p*q（p和q为大质数），1<e<ϕ(n)，gcd(e,ϕ(n))=1，ed % ϕ(n)=1，M为信息，C为密文数论基础质数：又称素数，一个大于1的自然数，除了1和它自身外，不能被其他自然数整除的数模运算：求余运算，整除运算中，求a除以b的余数，可用mod表示，即r = a mod b同余：两个整数a、b，若它们除以正整数m所得的余数相等，则称a、b相对于模m同余，记作: a≡b(mod m)最大公因数：指两个或多个整数共有因数中最大的一个，a和b的最大公因数记作：gcd(a,b)互质：若N个整数的最大公因数是1，我们就称这N个整数互质，即gcd(a,b)=1，a和b互质欧拉函数：对于正整数n，小于n的正整数中与n互质的数的个数，记为ϕ(n)，特别的ϕ(1)=1欧拉定理：若两个正整数a和m互质，那么a的φ(m)次方除以m的余数为1，即a^ϕ(m)≡1(mod m)模反元素：如果两个正整数a和n互质，则一定可以找到整数b，使得a×b-1被n整除，或者说ab≡1(mod n)，称b为a相对于n的模反元素比如，3和11互质，那么3相对于11的模反元素就是4，因为(3 × 4)-1可以被11整除a^(ϕ(n)-1)为a相当于n的模反元素，因a^ϕ(n)≡1(mod n) => a^(1+ϕ(n)-1)≡1(mod n) => a×a^(ϕ(n)-1)≡1(mod n)同余相关性质若a≡b(mod m)，b≡c(mod m)，则a±c≡b±d(mod m)若a≡b(mod m)，b≡c(mod m)，则a×c≡b×d(mod m)若a≡b(mod m)，那么a^n≡b^n(mod m)若a≡b(mod m)，n为m因数,则a≡b(mod n)互质相关性质任意两个质数是互质数较小数是质数，较大数不为它的倍数，这两个数是互质数较大数是质数的两个数是互质数1和任意一个自然数是是互质数2和任何奇数是互质数相邻的两个自然数是互质数相邻的两个奇数是互质数欧拉相关性质ϕ(1)=1，ϕ(2)=1当n是质数时，ϕ(n)=n-1当p为质数时，k为自然数，那么ϕ(p^k)=p^k - p^(k-1)当a和b互质时，ϕ(ab)=ϕ(a)ϕ(b)，故当n是奇数时，ϕ(2n)=ϕ(n)任意一个大于1的正整数，都可以写成一系列质数的积只有p的倍数与p^k互质，共有p^(k-1)个，即1p、2p、3p...p^(k-1)p，把它们去除，剩下的就是与p^k互质的数了，即ϕ(p^k)=p^k - p^(k-1)算法选择两个大质数p和q计算n=p×q，计算欧拉函数ϕ(n)=(p-1)×(q-1)选择一个数e，满足：1<e<ϕ(n)，且e与ϕ(n)互质计算e相对于ϕ(n)的模反元素d，即ed≡1(mod ϕ(n))公钥为{e，n}，私钥为{d，n}加密：C=M^e(mod n)，解密：M=C^d(mod n)，M为信息，C为密文，M需小于nRSA签名可用私钥加密信息摘要，验证者用公钥解密签名，和给出的签名进行一致性判断 *大质数生成随机构造一个满足最终大质数长度的奇数n判断n是不是质数，如果是则输出n，否则n=n+2并重复此步骤使用质数定义判断，将n除以2到n的算术平方根之间的所有数，如果余数都不为0，则此数一定为质数使用素性检测算法（Fermat、Miller–Rabin等等），多次判断此数都为质数的情况下，则此数极大概率为质数查找互质数利用互质特性直接得出结果（两个相邻的自然数是互质数，较小质数与不是倍数的较大数互质）使用辗转相除法计算两个数的最大公约数，如果是1，则两数互质计算模反元素随机选择一个正整数k根据定义：a×b % n = 1，则a相对于n的模反元素b = (k×n + 1) / a快速幕模算法计算a^b%n(a<n)，如果a和b很大，可以使用同余的性质来快速计算结果根据同余性质，有(a × a) % n = (a%n × a%n) % n若b为偶数，则a^b%n = (a^(b/2)%n × a^(b/2)%n) % n若b为奇数，则a^b%n = (a^(b/2)%n × a^(b/2)%n × a) % n令b' = b/2，有a^(b/2)%n = a^b'%n故这里可以使用递归的方式计算a^b%n，不断将b除2，直到1为止安全性随机数：使用完全随机的算法，避免潜在的安全，如果随机数被预测，私钥就有可能被破解密钥长度：为了保持算法的安全性，必须选择足够大的密钥长度，最低需要1024位，推荐使用2048位参数选择：尽量不重复使用大质数p和q，也不要选择太大或太小的e和d证明证明：M=C^d mod n (C=M^e mod n, 0<M<n)C^d mod n 代入C = M^e mod n = (M ^ e mod n)^d mod n 同余化简(a mod n) mod n = a mod n = M ^ (e × d) mod n 代入e × d = k × ϕ(n) + 1（k为任意正整数），因ed互为ϕ(n)的逆元 = M ^ (k × ϕ(n) + 1) mod n 等价转换 = M ^ (ϕ(n) × k) × M mod n 接下来需要证明此式等于M即可由n=p×q，M<n，可知M有三种情况：与n互质、p的倍数、q的倍数若M和n互质M ^ (ϕ(n) × k) × M mod n 代入M^ϕ(n) mod n = 1（欧拉定理） = 1^k × M mod n 代入1^k mod n = 1 = M mod n = M若M为p的倍数，则M必和q互质，M ^ (ϕ(n) × k) × M mod n 代入ϕ(n) = ϕ(p) × ϕ(q) = M ^ (ϕ(p) × ϕ(q) × k) × M mod n 因q为n的因数，使用a mod n = a mod q转换 = M ^ (ϕ(p) × ϕ(q) × k) × M mod q 代入M ^ ϕ(q) mod q = 1 （欧拉定理） = 1^(ϕ(p) × k) × M mod q 代入1^(ϕ(p) × k) mod q = 1 = M mod q = M同理可证M为q的倍数的情况
Restaking（再质押）是DeFi（去中心化金融）深化发展后的一种复杂产品形态，也是以太坊共识机制转PoS后才出现的新型金融衍生品。一、质押质押的概念出现于区块链的PoS共识机制（Poof of Staking，权益证明）。目前主要有两个主流的共识机制：PoW（Poof of Work，工作量证明）和PoS（Poof of Staking，权益证明）。区块链世界的鼻祖比特币就是采用的 PoW 共识机制。简单说，区块链相当于一个全球化的分布式账本，为了维护这个去中心化系统的运作，需要有人去算账和记账。以太坊区块链最开始也是采用的PoW共识机制，但是PoW也存在一些问题，比如消耗大量电力去计算数学题，没有产生任何对现实世界有价值的收益，而且出现了专门用来挖矿的设备以及具有规模化挖矿的矿场企业，趋向中心化了。因此后来出现了PoS共识机制（Poof of Staking，权益证明），就是想要获得记账权的人把代币质押到一个地方当作保证金，谁持有的代币更多和持有得更久，谁获得记账权的概率就越高，这样大家就不用消耗大量的算力来算没用的数学题了。质押就是让用户锁定一定资金作为保证金后成为节点，去维护项目的安全，从而赚取收益，若节点作恶则会罚没保证金。从技术视角看，质押是为了维持去中心化系统的自我运作PoW是通过算力占比来获得记账权；PoS是通过资产占比来获得记账权；从金融视角看，质押可以控制链上经济系统的货币发行PoW将代币直接奖励给矿工，矿工将链上资产换成现实世界资产买矿机，继续投入挖矿，实现了区块链世界和现实世界的融合；PoS将代币直接奖励给质押者，质押者通过质押资产获取了收益，质押资产变更了具有稳定收益率的 “国债”，衍生了各类金融玩法。质押方案独立质押：运行节点，成为质押者，需要32个ETH，这个是最基本的质押模式；质押即服务：运行节点很麻烦，需要一直在线，因此出现了质押即服务，同样需要32个ETH，但无需运行节点，将节点运作的工作委托给第三方运营商；联合质押32个ETH的门槛还是很高，因此出现了联合质押的平台，不需要32个ETH，每个人可以与其他人一起质押任意数量的 ETH，将节点运作委托给第三方；中心化交易所：使用联合质押的平台还是有一定门槛，用户需要懂得使用区块链应用，同时这类应用可能会存在合约风险，比如智能合约存在漏洞，被黑客攻击等等，因此出现中心化交易所帮助用户进行质押的需求，用户将资金完全交给交易所，交易所进行质押。这四种方案门槛是不断降低的，操作上也越来越简单，这样就可以触达更多的用户，当然，用户资产面临的风险也是越来越大的，越来越不受用户自己掌控。流动性质押（LSD）用户将资产质押了之后，资产就被锁在了链上，此时资金利用效率很低，因此出现了流动性质押平台（Liquid Staking Derivatives，LSD），比如Lido，当你质押了1个ETH，平台会1比1给你一个质押凭证，即1个stETH。这类流动性质押平台一般采用的是联合质押模式，用户可存入任意数量的ETH，帮助用户进行节点质押，用户可获得质押凭证，让用户可在DeFi中进行交易、借贷、提供流动性等操作，提升了资金使用效率。其实这可以类似银行存款，你将现金存到了银行，银行会给你一个存款凭证，然后你可以继续使用这个存款凭证做其他事情，比如交易、抵押借贷等。二、再质押除了区块链采用的PoS机制需要用户质押资产之外，很多项目也都需要Staking来保证项目安全，比如跨链桥、预言机、数据可用性层、零知识证明等。那么，每上线一个新项目，这个项目就需要用户锁定一定的资金，用户的流动资金就那么多，这就造成了不同项目之间的竞争。各个项目为了项目安全性不得不竞争市场上有限的流动资金，随着不同项目给出的质押收益率越来越高，项目自身承担的风险也越来越高，变成了一个恶性循环。另一方面，用户也只能把有限的资金选择有限的项目质押进去获得有限的回报，资金的利用率较低。随着公链、应用、各类项目越来越多，流动性也越来越割裂。因此，市场上出现了 “共享安全性” 的需求，需要有一个平台可以将用户质押的资产为多个项目提供安全性，这就是再质押（Restaking）出现的背景。类比现实世界，一些弱小的国家会引入强大国家的军事力量，自己则无需花费更多的资金构建自己专属的军事力量。目前安全性最强的PoS机制公链就是以太坊，以太坊上质押了大量的资金，具有极强的安全性，因此非常值得将以太坊的安全性共享给其他项目。另一方面，再质押可以为stETH这类质押凭证拓展了更多的应用场景，通过各类协议的可组合性，实现了以太坊生态的 “金融乐高”。再质押定义根据再质押出现的背景以及解决的问题，我们可以给一个再质押的定义：再质押的本质是建立一个共享质押池，从而实现共享安全性。这个共享资金池可以将一份资金同时为多个项目质押保证安全性，从而实现一鱼多吃，让资金与项目的关系从 1:1 变为 1:N。一方面，用户可以获得超额回报，另一方面减轻了项目争抢质押资金的压力。还有一种理解的视角是：Restaking是POS领域的合并挖矿。在 PoW 机制中，矿工可以通过挖掘使用同样Hash算法的主链和辅链来一鱼两吃，但是合并挖矿只提高了一定收益但没有继承安全性；但在PoS机制中，由于PoS有罚没机制，即作恶者节点的质押资金会被罚没，所以在Restaking中在辅链里作恶的行为会传回主链进行质押金的罚没，在这一点上可以继承主链的安全性。收益叠加对于用户来说，质押ETH得到LST（Liquid Staking Token），比如通过Lido等流动性质押协议获取的stETH；第二次质押LST得到LRT（Liquid Restaking Token），用户将stETH这类LST资产委托给流动性再质押协议，协议替用户将LST存入EigenLayer再质押，获得抵押凭证代币，即LRT资产，LRT可以进行再一次交易、借贷等等的金融操作。每多质押一次，就多了一次利用流动性博取收益的机会。风险加倍1、套娃风险牛市上升周期，风险会被对冲；但是到了熊市，在下行周期，风险就会被放大。多次质押，收益叠加，风险也会随之叠加。这就是以太坊创始人 Vitalik 所说的 “共识过载”：共享安全性后，劣势项目存在风险，最终给以太坊带来不可预知的风险。以太坊未来的终局是成为一条底层链，因此不能太复杂，需要保持简洁性，这类Restaking技术可能会产生连环爆雷从而影响以太坊安全。2、罚没风险以太坊质押机制中有 50% 的罚没风险，再质押协议对节点也有 50% 的罚没风险。因此，用户资金依旧会遇到资金罚没的风险，但是风险被均摊。3、可持续性风险没有能力做验证器的项目，项目风险可能比较大。这就像传统金融世界中的贷款，如果一个项目没有能力从银行贷款，那么说明这类项目的资质一般较差，风险较高，只能从其他渠道借高利贷。虽然项目给的利率高，但是风险也非常高。再质押赛道的新产品前期靠空投吸引质押资金，后期需要管控项目风险。最关键的是，再质押赛道的产品可以产生正向现金流，这样才能证明其模式具有可持续性。EigenLayer项目EigenLayer 是一个基于以太坊构建的协议，为以太坊生态引入了再质押的概念，用户可以将质押的 ETH 或 LST 重新质押，以此将以太坊的安全性扩展到以太坊生态中的其他应用。“共享安全性” 这个概念并非 EigenLayer 首创，Polkadot 的插槽方案、Cosmos 的链间安全共享、Avalanche 的子网都是安全共享的老玩家了，但 EigenLayer 首次在以太坊生态引入共享安全性。简单来说，EigenLayer 就是一个大型押金接单平台。EigenLayer 提供一个开放市场机制，让购买服务者可以根据自己的风险偏好自由选择自己需要的服务。而 EigenLayer 作为服务中介，将从 LSD 再次分配到中间件和基础设施中获得收益。除了 Restaking 业务之外，EigenLayer 还有一个产品叫 EigenDA，构建在 EigenLayer 之上。EigenDA 是 EigenLayer 上的第一个 AVS，可以理解为打样的自营 AVS，通过 EigenLayer 再质押提供的共享加密经济安全性，为 Rollup 提供经济高效、超大规模吞吐量的数据可用性。目前 EigenLayer 是再质押赛道的龙头项目，2024 年 2 月加密行业的头部投资机构 a16z 再次投资了 1 亿美元，估值可能达到百亿美元。再质押模式EigenLayer支持的再质押模式一共有4种：Native restaking：原生再质押，验证者可以将已经质押在以太坊中的 ETH 再次质押到 EigenLayer 中，此时一个 ETH 同时作为以太坊和 EigenLayer 的抵押品；这个过程是 L1→ EigenLayer。LST restaking：LST 再质押，用户将自己存入 LSD（流动性质押）协议后得到的 LST（质押凭证，如 stETH）存入 EigenLayer 再质押；这个过程是 DeFi →EigenLayer 。ETH LP restaking：ETH 金融衍生品再质押，这一种是第一种衍生出来的，即然在以太坊中质押的 ETH 可以再质押，那么只要包含 ETH 的金融资产也可以再质押，比如用户在去中心化交易所中提供流动性得到的包含 ETH 的存款凭证（LP 代币）；这个过程是 DeFi →EigenLayer 。LST LP restaking：LST LP 再质押，这一种是第二种衍生出来的，即然 LST（质押凭证，如 stETH）可以再质押，那么只要包含 LST 的金融资产也可以再质押，比如用户在去中心化交易所中提供流动性得到的包含 LST 资产的存款凭证（LP 代币）；这个过程是 L1 → DeFi→EigenLayer。从这四种质押模式可以看到，EigenLayer 支持和兼容了 ETH 资产以及 ETH 衍生出来的一系列资产，这些都被视为有价值的资产，可以用于再质押。系统架构EigenLayer整体可以分为4层，从下向上分别是：以太坊主网：EigenLayer构建在以太坊主网之上，因此最底层的基础是以太坊主网。中间层：中间层是AVS操作员（Operators），即节点运营商，在整个协议中是中间人的角色，Staker和AVS之间的桥梁，它帮助Staker管理资金，同时又帮助AVS执行具体任务。角色层质押者：为AVS提供资金以此赚取收益，将原生 ETH 或 LST ETH 重新抵押到 EigenLayer 协议的人；消费者：各种需要使用质押服务来保证安全性的项目；AVS开发者：在EigenLayer上构建自己的安全服务；治理层：EigenLayer的治理层；总的来说，Staker投入了资金成本，Operator投入了资源成本（运行节点所需的资源），AVS使用前面两者的服务，付出资金成本。
以太坊作为一个不断发展的区块链平台，随着时间的推移引入了多个改进提案来提升其性能和用户体验，其交易的数据结构也在一直“进化”交易类型过去，以太坊的交易都采用同一种格式：RLP([nonce, gas_price, gas_limit, to, value, data, v, r, s])在EIP-2718（2021年4月15日柏林（Berlin)升级）改进协议中，定义了一种带交易类型的通用交易格式：TransactionType || TransactionPayload将上述字段连接（合并）起来，即可得到一个类型化交易。字段的定义是：TransactionType：0至0x7f范围内的某个值，最多可代表128种事务类型。TransactionPayload：由事务类型定义的任意一个字节数组。EIP-2718没有为交易的有效负载定义格式。因此，交易的有效负载可以是任意一段经过编码的字节序列，只要采用符合新的交易类型（如RLP、SSZ等）定义的编码器即可。之所以选择简单的字节相连方式，是因为读取字节数组的第一个字节非常简单，无需使用任何库或工具。新的交易类型最多可达0x7f种。选择这一上限是为了保证向后兼容传统交易。经过RLP编码的交易的第一个字节始终大于或等于0xc0，因此类型化交易永远不会与传统交易产生冲突，而且类型化交易和传统交易之间可以通过第一个字节来区分。以太坊目前的源代码中定义了四种交易类型：// Transaction types. const ( LegacyTxType = 0x00 AccessListTxType = 0x01 DynamicFeeTxType = 0x02 BlobTxType = 0x03 )Legacy交易Legacy交易是以太坊早期的交易类型，从链开始持续很长时间都是此类型的交易。特点：Gas价格（Gas Price）：用户设定的固定值，表示愿意为每个Gas单位支付的费用。Gas限额（Gas Limit）：用户愿意为交易支付的最大Gas数量。交易费用：由 Gas Price * Gas Used 计算得出。示例：{ "from": "0xSenderAddress", "to": "0xRecipientAddress", "value": "1000000000000000000", // 1 ETH in Wei "gas": 21000, "gasPrice": "20000000000" // 20 Gwei }AccessList交易EIP-2930（2021年4月15日柏林（Berlin)升级）改进协议，引入了访问列表（Access List）功能，以优化Gas费和提高交易效率。增加了以下格式的交易：0x01 || RLP([chain_id, nonce, gas_price, gas_limit, to, value, data, access_list, y_parity, r, s])特点：访问列表（Access List）：提前声明交易将访问的存储和合约地址，减少Gas费。Gas价格和Gas限额：与Legacy交易类似，但增加了访问列表字段。示例：{ "from": "0xSenderAddress", "to": "0xRecipientAddress", "value": "1000000000000000000", // 1 ETH in Wei "gas": 21000, "gasPrice": "20000000000", // 20 Gwei "accessList": [ { "address": "0xContractAddress", "storageKeys": ["0xStorageKey1", "0xStorageKey2"] } ] }DynamicFee交易EIP-1559（2021年8月5日伦敦（London)升级）改进协议，引入动态费用功能，旨在改善Gas费的预测和用户体验。增加了以下格式的交易：0x02 || RLP([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, y_parity, r, s])特点：基础费用（Base Fee）：由网络动态调整，不会支付给矿工，而是被销毁。优先费（Priority Fee）：支付给矿工的费用，类似于小费，以激励矿工优先处理交易。最大费用（Max Fee）：用户愿意支付的最高费用，包括基础费用和优先费。示例：{ "from": "0xSenderAddress", "to": "0xRecipientAddress", "value": "1000000000000000000", // 1 ETH in Wei "maxFeePerGas": "30000000000", // 30 Gwei "maxPriorityFeePerGas": "2000000000", // 2 Gwei "gasLimit": 21000 }Blob交易EIP-4844（2024年3月13日坎昆（Cancun）升级）改进协议，可以将L2分片数据以廉价的方式提交到链上，旨在大幅提升以太坊的扩展性。增加了以下格式的交易：0x03 || RLP([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, max_fee_per_blob_gas, blob_versioned_hashes, y_parity, r, s]) 特点：数据分片（Data Sharding）：引入数据分片来分担以太坊主链的负载，大幅提高交易吞吐量。Blob数据：EIP-4844引入了新的交易类型，包含“blob”数据，这些数据不会永久存储在以太坊节点中，而是短期存储，提高数据处理效率。示例：{ "from": "0xSenderAddress", "to": "0xRecipientAddress", "value": "1000000000000000000", // 1 ETH in Wei "gas": 21000, "maxFeePerGas": "30000000000", // 30 Gwei "maxPriorityFeePerGas": "2000000000", // 2 Gwei "maxFeePerBlobGas": "3000000000", // 3 Gwei "blobVersionedHashes": "0xBlobData" }主要区别总结Gas费用机制：Legacy：用户设定固定的Gas价格。EIP-2930：类似Legacy，但增加了访问列表以优化Gas费。EIP-1559：引入动态调整的基础费用和优先费机制。EIP-4844：引入数据分片和blob数据。新功能：Legacy：无特别优化，使用最早的交易机制。EIP-2930：通过访问列表优化Gas费和交易效率。EIP-1559：改善Gas费预测和用户体验。EIP-4844：大幅提升扩展性，引入数据分片和blob数据。
说明以太坊区块链主要里程碑、分叉和更新的时间表分叉是区块链协议升级或修改，此时区块链会分成“两条链”，一条链继续使用旧的协议，另一条链则应用新的协议。如果协议向后兼容则为软分叉，否则为硬分叉（新协议可正常处理旧协议生成的区块）关键时间表2015年7月30日以太坊主网启动，开始公测，这个初始版本有诸多限制，在上面能做的事情很少，主要面向开发者。2016年3月14日家园分叉，包括若干协议修改和联网变更，相比初始版本来说稳定很多。2017年10月16日拜占庭升级，将区块挖矿奖励从5个以太币减少到3个以太币，修改完善虚拟机。2019年12月8日伊斯坦布尔分叉，优化了虚拟机中特定操作的燃料成本，添加CHAINID虚拟机指令2020年12月1日信标链启动，使用权益证明机制，未来将会替代以太坊的工作量证明机制2022年9月15日巴黎升级（合并），是以太坊历史上最重要的升级！结束了工作量证明挖矿算法及相关共识逻辑，使用信标链作为共识引擎，一个完整的节点包括执行客户端（原以太坊）和共识客户端。所有时间表2013年11月27日：发布白皮书2014年04月01日：发布黄皮书2014年07月22日：预售以太币2015年07月30日：发布第一个版本，前沿（Frontier）2015年09月07日：200,000区块高度，前沿解冻分叉2016年03月14日：1,150,000区块高度，家园（Homestead）分叉2016年07月20日：1,920,000区块高度，去中心化自治组织（DAO）分叉2016年10月18日：2,463,000区块高度，橘子口哨(Tangerine Whistle)分叉2016年11月22日：2,675,000区块高度，伪龙(Spurious Dragon)分叉2017年10月16日：4,370,000区块高度，拜占庭（Byzantium）升级2019年02月28日：7,280,000区块高度，君士坦丁堡（Constantinople）分叉2019年12月08日：9,069,000区块高度，伊斯坦布尔（Istanbul）分叉2020年01月02日：9,200,000区块高度，缪尔冰川（Muir Glacier）升级2020年10月14日：11,052,984区块高度，部署质押存款合约2020年12月01日：0纪元，信标链创世2021年04月15日：12,244,00区块高度，柏林（Berlin）升级2021年08月05日：12,965,00区块高度，伦敦（London）升级2021年10月27日：74,240纪元，天鹰座（Altair）升级2021年12月09日：13,773,000区块高度，箭形冰川（Arrow Glacier）升级2022年06月30日：15,050,000区块高度，灰色冰川（Gray Glacier）升级2022年09月06日：144,896纪元，Bellatrix升级2022年09月15日：15,537,394区块高度，巴黎（Paris）合并2023年04月12日：17,034,870区块高度（194,048纪元），上海嘉佩乐 (Shapella)升级，包括上海（Shanghai）升级和卡佩拉（Capella）升级2024年03月13日：19,426,587区块高度（269,568纪元），坎昆-天津四(Dencun)升级，包括坎昆（Cancun）升级和天津四（Deneb）升级参考链接：https://ethereum.org/zh/historyhttps://eips.ethereum.org/meta
默认情况下，Python的依赖包会安装到相同的位置，且不同版本的包不能共存。如果同时开发或运行不同的项目，则可能会存在包冲突问题，要么修改代码使不同的项目都使用相同版本的包，要么使用虚拟环境。Python虚拟环境是一种用于隔离不同Python项目所需依赖的机制。它允许在同一台机器上同时管理多个Python项目，每个项目都可以有自己独立的Python版本和依赖库，互不干扰。使用虚拟环境的好处在于：可以帮我们快速创建干净、完全隔离的且不同版本的Python解释器以便我们能在不同项目中开发；可以避免因在不同项目中只使用唯一的全局解释器而导致的依赖冲突问题（Dependency Conflicts）。激活虚拟环境后，python和pip等命令将作用于虚拟环境一、 venvvenv是Python官方在Python3.3版本开始内置的一个标准库模块，让我们可以快速生成一个虚拟环境。现在我们只要下载的Python 解释器版本在3.3及以上就能直接使用，而无需安装其他依赖。使用它的方式十分简单，只需要通过使用对应版本的Python解释器并运行以下命令即可，比如这里我们以Python 3.10版本解释器为例：$ python3.10 -m venv <your-virtual-environment-name>通常我们在创建虚拟环境时需，要简单指定一个名称用以存放虚拟环境的内容，所以上述代码中的名称通常就保持和venv模块名称一致：$ python3.10 -m venv venv通过对应版本的Python解释器调用venv模块得到的虚拟环境，其版本也与解释器保持一致，也即在创建的venv目录中期Python解释器也为3.10。并且在不指定目录的情况下，默认会在当前运行命令的目录中创建虚拟环境。目前像Pycharm这样的IDE或VS Code类似的文本编辑器（需要安装插件）已经会正确识别到venv目录，并在运行代码时自动帮我们切换到虚拟环境中。但如果我们是在终端上切换虚拟环境，那么就需要自己进行手动激活（Active）。由于venv模块创建的虚拟环境目录在Linux和Windows内容有所不同，所以就导致二者在激活虚拟环境时的命令也有所差异。# Linux $ source venv/bin/activate # Windows $ .\venv\Scripts\activate.bat当然，如果我们想要退出虚拟环境，要么直接关闭IDE，要么直接在终端命令行界面输入deactivate命令即可（或是同样关闭终端）。$ deactivateactivate文件有多个版本，当在windows系统时，如果用CMD时，使用activate.bat；如果用PowerShell时使用activate.psl二、 pipenvpipenv将包管理工具pip和venv有机地结合到了一起，实现了开发环境和生产环境依赖的分离，旨在构建专属于Python的开发工作流，达到类似于npm，yarn等工具的效果。安装:pip install pipenv基本用法：创建环境：pipenv install激活环境：pipenv shell退出环境：exit删除环境：pipenv --rmpipenv install命令会根据系统默认Python版本来创建虚拟环境，并生成用于依赖包管理的Pipfile和Pipfile.lock文件。当你将项目分享给他人的时候，执行pipenv install命令会创建一个虚拟环境并自动安装Pipfile中指定的依赖包。和npm的pack-lock.json类似，pipenv通过依赖描述文件Pipfile来安装和管理依赖项目包管理：安装requests：pipenv install requests安装所有依赖包：pipenv install卸载requests：pipenv uninstall requests卸载所有依赖包：pipenv uninstall --all
stripe一体化全球支付平台，有良好的用户体验和界面设计，支持多种接入方式，且功能比较灵活，也易于集成。注册和API密钥账户注册验证激活访问Stripe网站：https://stripe.com/zh-hk ，并点击登录按钮登录，没有账号的可以先注册一个。填写必要信息：提供您的电子邮件地址、密码等信息以注册账户，并完成身份验证步骤，以确保账户的安全性。集成stripe支付，一开始账户处于开发模式，所有数据都是模拟的，上线时录入商家或公司信息激活账户进入真实模式。API密钥获取登录Stripe控制台：https://dashboard.stripe.com ，使用您的账户信息登录Stripe控制台。进入开发人员界面，单击API密钥选项卡，在下面可以找到公钥和密钥。在开发阶段，使用测试密钥，在生产环境中使用生产密钥，注意保管好密钥。妥善保管这些凭据，并根据需要定期更新它们，以维护支付系统的安全性集成前后端这里使用go语言为后端语言，react为前端语言。一、设置服务器将依赖项添加到您的项目并导入库。go get -u github.com/stripe/stripe-go/v78接下来创建一个结账接口，前端客户请求这个接口时，按客户的要购买的商品来创建一个结账会话，并将这个会话的ClientSecret返回给客户。import ( "log" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v78" "github.com/stripe/stripe-go/v78/checkout/session" ) func init() { // This is your test secret API key. stripe.Key = "sk_test_51P12rrI3yx14f44QTHOPqliw9So8GBpZ6EIPi7v1iDbdlgr5NH0vXIH3zUjQJwxLI7pbAsfSErQOy75sAVr7q4ra00AODgXKD0" } func CreateCheckoutSession(c *gin.Context) { var req struct { Email string `json:"email"` Amount int64 `json:"amount"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "msg": "param decode error:" + err.Error(), }) } params := &stripe.CheckoutSessionParams{ LineItems: []*stripe.CheckoutSessionLineItemParams{ { PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{ Currency: stripe.String("usd"), UnitAmount: stripe.Int64(10000 * req.Amount), ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{ Name: stripe.String("AI Tools"), }, }, Quantity: stripe.Int64(1), }, }, CustomerEmail: stripe.String(req.Email), Metadata: map[string]string{"email": req.Email, "amount": strconv.Itoa(int(req.Amount))}, Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), UIMode: stripe.String("embedded"), ReturnURL: stripe.String("http://localhost:5173/#/payment?session_id={CHECKOUT_SESSION_ID}"), } s, err := session.New(params) if err != nil { log.Printf("session.New: %v", err) } c.JSON(http.StatusOK, gin.H{ "clientSecret": s.ClientSecret, }) }按客户实际请求填充stripe.CheckoutSessionParams，可以参考上面代码，修改产品和价格即可。另外需要一个会话详情接口，用来辅助前端查看结账会话是否已经支付。func RetrieveCheckoutSession(c *gin.Context) { s, _ := session.Get(c.Query("session_id"), nil) c.JSON(http.StatusOK, gin.H{ "status": string(s.Status), "customer_email": string(s.CustomerDetails.Email), }) }最后再配置路由端口 router := gin.Default() router.POST("/create-checkout-session", CreateCheckoutSession) router.GET("/session-status", RetrieveCheckoutSession)二、设置前端将Stripe添加到React应用程序中npm install --save @stripe/react-stripe-js @stripe/stripe-js编写一个支付组件和结果组件，要付款时展示支付组件，支付完成后展示结果组件，调用后端结账接口完成支付功能。import { Button, Card, Checkbox, Form, Input, InputNumber, message, theme } from 'antd' import { useCallback, useEffect, useState } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { Api } from '@api/index' import { loadStripe } from '@stripe/stripe-js'; import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js'; import { getLocalStorage } from '@utils/publicFn' const stripePromise = loadStripe("pk_test_51P12rrI3yx14f44QGrlx7wtiyuwt969ttal5rfsSKOqxvoL3iUt3QlTft9b05ZFNea6KQvSriOeESbbBbJOba6nN00LXBpdQ9r"); const CheckoutForm = ({ email, amount }) => { const fetchClientSecret = useCallback(async () => { return (await Api.createCheckoutSession({ email, amount })).clientSecret }, []); return ( <EmbeddedCheckoutProvider stripe={stripePromise} options={{ fetchClientSecret }} > <EmbeddedCheckout /> </EmbeddedCheckoutProvider> ) } const Complete = ({ customerEmail }) => { const navigate = useNavigate(); return ( We appreciate your business! A confirmation email will be sent to {customerEmail}. If you have any questions, please email orders@example.com. navigate('/payment')}>返回 ) } const Payment = () => { const navigate = useNavigate(); const { token: { colorBgContainer }, } = theme.useToken() const [open, setOpen] = useState(false); const [status, setStatus] = useState(null); const [expiration, setExpiration] = useState(0); const [customerEmail, setCustomerEmail] = useState(''); const [email, setEmail] = useState(getLocalStorage('user')); const [amount, setAmount] = useState(1); const queryString = useLocation().search; useEffect(() => { Api.userProfile() .then((data) => { setExpiration(data.expiration) }) }, []) useEffect(() => { const urlParams = new URLSearchParams(queryString); const sessionId = urlParams.get('session_id'); if (sessionId) { Api.sessionStatus({ session_id: sessionId }) .then((data) => { setStatus(data.status); setCustomerEmail(data.customer_email); }); } else { setStatus(null); setCustomerEmail(''); setOpen(false) } }, [queryString]); const handlePay = ({ email, amount }) => { setEmail(email) setAmount(parseInt(amount)) setOpen(true) } return ( {(!open && !status) && <> <Card> 套餐到期时间：{new Date(expiration * 1000).toLocaleString()} navigate('/')}>返回首页 </Card> <Card> <Form.Item label="邮箱" name="email"><Input /></Form.Item> <Form.Item label="数量" name="amount"><InputNumber style={{ width: "100%" }} addonAfter={"年"} min={1} max={100} /></Form.Item> <Form.Item> 支付 </Form.Item> </Card> </>} {(open && !status) && <CheckoutForm email={email} amount={amount} />} {status && <Complete customerEmail={customerEmail} />} ) } export default Payment测试时可以使用测试卡号完成支付：4242424242424242，日期任意一个未来日期即可，CVC为任意三位数三、履行订单在用户支付完成后，我们需要履行订单，比如给用户发货、充值卡点、续费套餐等等。stripe推荐使用Webhook HTTP端点来接收事件，也可以使用事件接口轮询。下面的示例中，处理结账会话支付完成事件，给用户的VIP套餐续期。import ( "encoding/json" "log" "net/http" "strconv" "time" "server/model" "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v78" "github.com/stripe/stripe-go/v78/checkout/session" "github.com/stripe/stripe-go/v78/event" ) func init() { stripe.Key = "sk_test_51P12rrI3yx14f44QTHOPqliw9So8GBpZ6EIPi7v1iDbdlgr5NH0vXIH3zUjQJwxLI7pbAsfSErQOy75sAVr7q4ra00AODgXKD0" go func() { d := 5 * time.Second for { time.Sleep(d) events := event.List(&stripe.EventListParams{ CreatedRange: &stripe.RangeQueryParams{ GreaterThanOrEqual: time.Now().Add(-d).Unix(), }, Type: stripe.String(string(stripe.EventTypeCheckoutSessionCompleted)), }) for events.Next() { event := events.Event() if event.Type == stripe.EventTypeCheckoutSessionCompleted { var session stripe.CheckoutSession json.Unmarshal(event.Data.Raw, &session) id, email, amountStr := session.ID, session.Metadata["email"], session.Metadata["amount"] amount, _ := strconv.Atoi(amountStr) log.Println("order completed:", id, email, amount) expiration, err := model.SelectUserExpiration(email) if err == nil { now := time.Now().Unix() if expiration == nil || *expiration < now { expiration = &now } *expiration = time.Unix(*expiration, 0).AddDate(amount, 0, 0).Unix() model.UpdateUserExpiration(email, *expiration) } else { log.Println("Select Expiration Error:", err) } } } } }() }
路由Gin路由遵循Restful API接口规范，是基于httproute实现的基本路由语法如下，httpMethod为请求类型，relativePath为路径，handlers为该路由响应函数列表（多个中间件加一个执行函数）router.<httpMethod>(relativePath string, handlers ...HandlerFunc)支持GET、POST、PUT、PATCH、DELETE、OPTIONS等请求类型，Any方法匹配所有类型，Match方法匹配指定的多个类型func main() { router := gin.Default() router.GET("/login", loginEndpoint) router.POST("/login", loginEndpoint) router.Any("/login", loginEndpoint) router.MatchMatch([]string{"POST","GET"}, "/login", loginEndpoint) router.Run(":8080") }路径规则如下"/path"，指定路径，只能匹配/path"/:path"，参数路由，可以匹配/a和/b等，不能匹配/a/b"/*path"，任意路由，可以匹配/、/a和/a/b等所有路由路由按指定路径优先进行匹配，参数路由和任意路由在同一级不可共存func main() { router := gin.Default() // 匹配/user/john但不会匹配/user/或者/user router.GET("/user/:name", func(c *gin.Context) { name := c.Param("name") c.String(http.StatusOK, "Hello %s", name) }) // 将匹配/user/john/和/user/john/send // 如果没有其他路由匹配 /user/john，它将重定向到 /user/john/ router.GET("/user/:name/*action", func(c *gin.Context) { name := c.Param("name") action := c.Param("action") message := name + " is " + action c.String(http.StatusOK, message) }) router.Run(":8080") }路由分组func main() { router := gin.Default() // 简单的路由组: v1 v1 := router.Group("/v1") { v1.POST("/login", loginEndpoint) v1.POST("/submit", submitEndpoint) } // 简单的路由组: v2 v2 := router.Group("/v2") { v2.POST("/login", loginEndpoint) v2.POST("/submit", submitEndpoint) } router.Run(":8080") }路由中间件Gin框架是洋葱结构的，可以同时使用多个中间件，按注册顺序执行，中间件中使用c.Next()函数调用下一个中间件或路由响应函数，如果没有使用c.Next()则执行完此中间件将会返回func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 请求前 c.Next() // 请求后 latency := time.Since(t) log.Print(latency) } } func main() { r := gin.New() r.Use(Logger()) r.GET("/test", func(c *gin.Context) { }) r.Run(":8080") }中间件可以给指定路由用，比如r.GET("/test", Logger, loginEndpoint)代理拼接真实服务器链接，使用http请求资源，再使用DataFromReader方法返回资源const apiAddr = "https://example.com" func Proxy(c *gin.Context) { url, err := url.Parse(apiAddr + c.Request.URL.Path) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "err": err.Error(), }) } resp, err := http.DefaultClient.Do(&http.Request{ URL: url, Method: c.Request.Method, Body: c.Request.Body, Header: c.Request.Header, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "err": err.Error(), }) } defer resp.Body.Close() c.DataFromReader(resp.StatusCode, resp.ContentLength, resp.Header.Get("Content-Type"), resp.Body, nil) }使用方法，注意：使用Any匹配所有HTTP类型的请求，使用/*path匹配所有路径，可加入鉴权中间件限制代理的访问router.Any("/:api/*path", Proxy)
简化HTTP服务器的配置和管理，使用系统自带的服务，无需安装任何东西，将指定目录作为HTTP服务器的根目录，一键开启静态服务器实现原理Window10和Window11自带PowerShell，使用HttpListener类创建HTTP服务，绑定端口接收请求并返回指定文件。Linux系统自带python，用http.server模块即可创建简易的HTTP服务器。Windows系统实现创建server.ps1文件，并写入如下内容# PowerShell Web Server Param( [parameter(Mandatory=$false)][Int]$port=8080, [parameter(Mandatory=$false)][String]$webPath="www" ) Add-Type -AssemblyName System.Web $scriptPath = $PSScriptRoot $http = [System.Net.HttpListener]::new() $http.Prefixes.Add("http://localhost:$port/") $http.Start() function ConvertTo-Base64($str){ $result = [Convert]::ToBase64String([System.Text.UTF8Encoding]::UTF8.GetBytes($str)) return $result } function ConvertFrom-Base64($str){ $byteArray = [Convert]::FromBase64String($str) return [System.Text.UTF8Encoding]::UTF8.GetString($byteArray) } function Send-WebResponse($context, $content) { if($content.GetType().Name -ne "String"){ $content = ConvertTo-JSON $content } $buffer = [System.Text.Encoding]::UTF8.GetBytes($content) $context.Response.ContentLength64 = $buffer.Length $context.Response.OutputStream.Write($buffer, 0, $buffer.Length) } function Get-PostData{ $reader = new-object System.IO.StreamReader($context.Request.InputStream) $text = $reader.ReadToEnd() # $text = $text.Replace("%3D","=") # $text = ConvertFrom-Base64 $text $text = [System.Web.HttpUtility]::UrlDecode($text) Write-Host "$text" return ConvertFrom-Json $text } if ($http.IsListening) { write-host "HTTP Server Ready! " -f 'black' -b 'gre' write-host "$($http.Prefixes)" -f 'y' } # INFINTE LOOP, Used to listen for requests while ($http.IsListening) { $context = $http.GetContext() $RequestUrl = $context.Request.Url.LocalPath Write-Host "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) : $($context.Request.Url)" -f 'mag' # Get Request Url if ($context.Request.HttpMethod -eq 'GET') { # Redirect root to index.html if($RequestUrl -eq "/") { $RequestUrl = "/index.html" } if(Test-Path "$scriptPath\$webPath\$RequestUrl"){ $fileStream = [System.IO.File]::OpenRead( "$scriptPath\$webPath\$RequestUrl" ) $fileStream.CopyTo( $Context.Response.OutputStream ) $fileStream.Close() } else{ Send-WebResponse $context "404 : Not found $RequestUrl" } $context.Response.Close() } }脚本接收两个参数，第一个是绑定端口，默认为8080，第二个是指定根目录，默认为当前目录下的www\目录。打开PowerShell窗口，执行.\server.ps1，也可以右键菜单使用PowerShell执行此脚本，出现下图窗口表示执行成功，关闭窗口就可以关闭HTTP服务器。如果遇到执行异常，可以管理员身份打开PowerShell，运行命令Set-ExecutionPolicy RemoteSigned解锁ps1脚本执行权限（系统默认没有开启）。为了方便，可以创建如下内容的的start.bat文件，一键开启静态服务器，无需管理员身份，也没有权限问题。if "%1" neq "" (set port=%1) else (set port=8080) if "%2" neq "" (set root=%2) else (set root=www) start http://localhost:%port% powershell -ExecutionPolicy RemoteSigned .\server.ps1 %port% %root%Linux系统实现相比Windows系统简单许多，Linux系统默认已经安装python，使用http.server模块，一条命令可启动。绑定本地网卡地址，监听8080端口，使用www作为根目录，开启HTTP服务器的命令如下。python -m http.server 8080 -b localhost -d www可选参数说明-b：指定绑定网卡地址，默认为0.0.0.0，不安全，可更改为localhost-d：指定根目录，默认为当前目录为了方便，可以创建如下内容的的start.sh文件，一键开启静态服务器。#!/bin/bash xdg-open http://localhost:8080 python -m http.server 8080 -b localhost -d www