使用 sCrypt 实现一种可溯源的 Token 方案

本文提出了一种改进的基于UTXO的层一token方案,旨在防止重放攻击和中间人攻击。通过在每个tokenUTXO中嵌入全局唯一的tokenID和验证父及祖父交易,实现了无需信任第三方的高效验证。这种方法简化了token的历史交易验证,仅需回溯两步即可确保交易合法性,为层一token的广泛应用提供了可能。
摘要由CSDN通过智能技术生成

在此之前,我们发布了一个基于 UTXO 的 token 方案,其中包括可替换 token不可替换 token。像发行和转账这样的 token 规则都在层一由矿工来执行。但是,像 token 认证这样的额外规则,则需要在层二执行。这满足不了那些希望自己验证所有内容(如SPV)的用户的需求。我们设计了新的方案来解决这个问题,让所有的规则都可以在层一进行验证。

问题

假冒攻击

有两种方法来假冒一个 token,如上图所示。每个框都表示一个 Tx,左侧是 input,右侧是 output。箭头从一个 Tx 指向它花费的 Tx。有相同 output 颜色的框包含相同的合约代码。

  1. 重放攻击:攻击者可以发行一个同样的 token。
  2. 中间人攻击:攻击者可以从一个和 token 无关的 UTXO 开始,从该点构造出一个假的 token 转账链。

在所有三种情况下,Tx 都会被矿工接受,因为这些 Tx 都通过了层一验证。每种情况的最后那个在红色框里的 Tx ,看上去是彼此相同的。判断它是否是合法的token A 的唯一方法就是回溯到发行 Tx。这可以通过两种方式来实现:

  • 接收方自己进行所有验证。随着 token 转账链变长,这种方式很快会变得低效。
  • 接收方信任第三方来进行验证,这破坏了层一 token 方案的无需信任优势。

解决方案

我们想出了一个方案来防止这两种攻击。

使用 TXID 作为唯一的token ID

为了防止重放攻击,我们在每个 token UTXO 中用算法嵌入一个全局唯一的token ID。在token合约中,当发行 UTXO 被花费时,发行 Tx 的 txid 会被拷贝并作为 token ID 保留在后续所有的 token UTXO 中。任何一个新发行的token都会有不同的发行txid即不同的token ID,所以可以很容易检测出来。

回溯两步来验证

当收到一个 Tx 时,为了防止中间人攻击,我们不仅要验证父 Tx ,还要验证祖父 Tx。当假冒的token UTXO(UTXO1)被花费到 UTXO(UTXO2),它会在层一被验证通过,因为token合约并没有被运行(请注意,一个UTXO的锁定脚本只有在被解锁时才会被运行),所以 Tx 会被接受。然而,当 UTXO2 被花费到 UTXO3 时,除 UTXO2 外,UTXO1 也会被验证。验证将会失败,因为 UTXO1 没有包含与 UTXO2 和 UTXO3 相同的合约,因此 Tx 也将会被拒绝。因此,在验证一个token Tx 时(如果我们收到 UTXO2),只需再回溯一步就可以了,而不需要像之前那样一直回溯。这极大简化了 token 的实现。

实现

下图是token合约参考实现,代码含义参考注释。它不仅验了父 Tx,还验证了祖父 Tx。

import "util.scrypt";
/**
 * A non-fungible token enforced by miners at layer 1
 */
contract SPVToken {
    // prevTx: tx being spent by the current tx
    // prevPrevTx: tx being spent by prevTx
    public function transfer(Sig senderSig, PubKey receiver, int satoshiAmount, SigHashPreimage txPreimage, bytes prevTx, bytes prevPrevTx) {
        // constants
        int TokenIdLen = Constants.TxIdLen;
        int PrevTxIdIdx = 5;
        int UnlockingScriptIdx = 41;
        // uninitialized token ID
        bytes NullTokenId = num2bin(0, TokenIdLen);

        require(Tx.checkPreimage(txPreimage));

        // read previous locking script: codePart + OP_RETURN + tokenID + ownerPublicKey
        bytes lockingScript = SigHash.scriptCode(txPreimage);
        int scriptLen = len(lockingScript);

        // constant part of locking script: upto OP_RETURN
        int constStart = scriptLen - TokenIdLen - Constants.PubKeyLen;
        bytes constPart = lockingScript[: constStart];

        PubKey sender = PubKey(lockingScript[constStart + TokenIdLen :]);
        // authorize
        require(checkSig(senderSig, sender));

        bytes outpoint = SigHash.outpoint(txPreimage);
        bytes prevTxId = outpoint[: TokenIdLen];
        require(hash256(prevTx) == prevTxId);

        bytes tokenId = lockingScript[constStart : constStart + TokenIdLen];
        if (tokenId == NullTokenId) {
            // get prevTxId and use it to initialize token ID
            tokenId = prevTxId;
        }
        else {
            /*
            * validate not only the parent tx (prevTx), but also its parent tx (prevPrevTx)
            */

            // TODO: assume 1 input, to extend to multiple inputs
            bytes prevPrevTxId = prevTx[PrevTxIdIdx : PrevTxIdIdx + Constants.TxIdLen];
            require(hash256(prevPrevTx) == prevPrevTxId);

            int unlockingScriptLen = Util.readVarintLen(prevPrevTx[UnlockingScriptIdx :]);

            // TODO: only validate output 0 here, to extend to multiple outputs
            int lockingScriptIdx = UnlockingScriptIdx + unlockingScriptLen + Constants.InputSeqLen + 1 /* output count length */ + Constants.OutputValueLen;
            bytes prevLockingScript = Utils.readVarint(prevPrevTx[lockingScriptIdx :]);
            // ensure prev tx uses the same contract code
            require(len(prevLockingScript) == len(lockingScript));
            require(prevLockingScript[: constStart] == constPart);
            // belongs to the same token
            bytes prevTokenId = prevLockingScript[constStart : constStart + TokenIdLen];
            require(prevTokenId == tokenId || prevTokenId == NullTokenId);
        }

        // validate parent tx
        bytes outputScript = constPart + tokenId + receiver;

        bytes output = Utils.buildOutput(outputScript, satoshiAmount);
        require(hash256(output) == SigHash.hashOutputs(txPreimage));
    }
}

总结

一般认为,基于 UTXO 的层一 token方案需要回溯历史 Tx 直到回溯至发行 Tx 。我们提出了一个突破方案,只需要回溯两步即可。该方案并不需要信任第三方,为更广泛的层一token普及铺平了道路。同样的技术也适用于其他智能合约,来避免对 Tx 历史的过多回溯。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sCrypt Web3应用开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值