二十七.签名与脚本(2)--脚本原理

1.CScript"<<"重载

我们看到之前写脚本的方式,都是通过<<符号将脚本写进CScript里面。

CScript继承 public vector<unsigned char> 如下:

cpp 复制代码
class CScript : public vector<unsigned char>
{
protected:
    CScript& push_int64(int64 n)

所以它是一个容器,vector数组,用来储存脚本代码。

然后通过重载<<操作符,来将脚本写进容器里,我们可以看到<<重载,参数分为分很多类型,支持很多类型代码的写进script里:

cpp 复制代码
    CScript& operator<<(char b) { return (push_int64(b)); }
    CScript& operator<<(short b) { return (push_int64(b)); }
    CScript& operator<<(int b) { return (push_int64(b)); }
    CScript& operator<<(long b) { return (push_int64(b)); }
    CScript& operator<<(int64 b) { return (push_int64(b)); }
    CScript& operator<<(unsigned char b) { return (push_uint64(b)); }
    CScript& operator<<(unsigned int b) { return (push_uint64(b)); }
    CScript& operator<<(unsigned short b) { return (push_uint64(b)); }
    CScript& operator<<(unsigned long b) { return (push_uint64(b)); }
    CScript& operator<<(uint64 b) { return (push_uint64(b)); }

    CScript& operator<<(opcodetype opcode)
    {
        if (opcode <= OP_SINGLEBYTE_END)
        {
            insert(end(), (unsigned char)opcode);
        }
        else
        {
            assert(opcode >= OP_DOUBLEBYTE_BEGIN);
            insert(end(), (unsigned char)(opcode >> 8));
            insert(end(), (unsigned char)(opcode & 0xFF));
        }
        return (*this);
    }

    CScript& operator<<(const uint160& b)
    {
        insert(end(), sizeof(b));
        insert(end(), (unsigned char*)&b, (unsigned char*)&b + sizeof(b));
        return (*this);
    }

    CScript& operator<<(const uint256& b)
    {
        insert(end(), sizeof(b));
        insert(end(), (unsigned char*)&b, (unsigned char*)&b + sizeof(b));
        return (*this);
    }

    CScript& operator<<(const CBigNum& b)
    {
        *this << b.getvch();
        return (*this);
    }

    CScript& operator<<(const vector<unsigned char>& b)
    {
        if (b.size() < OP_PUSHDATA1)
        {
            insert(end(), (unsigned char)b.size());
        }
        else if (b.size() <= 0xff)
        {
            insert(end(), OP_PUSHDATA1);
            insert(end(), (unsigned char)b.size());
        }
        else
        {
            insert(end(), OP_PUSHDATA2);
            unsigned short nSize = b.size();
            insert(end(), (unsigned char*)&nSize, (unsigned char*)&nSize + sizeof(nSize));
        }
        insert(end(), b.begin(), b.end());
        return (*this);
    }

    CScript& operator<<(const CScript& b)
    {
        // I'm not sure if this should push the script or concatenate scripts.
        // If there's ever a use for pushing a script onto a script, delete this member fn
        assert(("warning: pushing a CScript onto a CScript with << is probably not intended, use + to concatenate", false));
        return (*this);
    }

底层都是通过push_back或insert将这些数据插入到vector容器里。

其中一些常用的整型类型是调用了下面这两个函数,用push_back的方式(小于等于16):

cpp 复制代码
protected:
    CScript& push_int64(int64 n)
    {
        if (n == -1 || (n >= 1 && n <= 16))
        {
            push_back(n + (OP_1 - 1));
        }
        else
        {
            CBigNum bn(n);
            *this << bn.getvch();
        }
        return (*this);
    }

    CScript& push_uint64(uint64 n)
    {
        if (n == -1 || (n >= 1 && n <= 16))
        {
            push_back(n + (OP_1 - 1));
        }
        else
        {
            CBigNum bn(n);
            *this << bn.getvch();
        }
        return (*this);
    }

关于这里为什么还带个操作码op_1-1,这只是一些优化,当数字小于等于16时,就把它换算成1个字节的操作码,能节省空间(不需要长度标记+字节的描述方式来写入数字)。现在我们大概了解即可。后续看情况再详细说明其实原理。

当解析器读脚本时,是1个字节读取的,那如果有一段数据,比如说是32字节,一个哈希值,那你怎么确定这段数据的起始和结束位置呢?

所以他会在数据前面加标记,指示压入多少数据到栈中。

比如2 5 op_add这个脚本,我们看到前面并没有标记呢?这是一种高级的表达方式。事实上

在写入2到script里面,底层函数会给前面加上标记(仅举例,以实际情况为准,见下面备忘录),表明后面将是数据,读取多少字节。

而如果符合条件16以下的数字,那么转换成操作码直接写入(如转换成op_1,直接知道要写入1到栈顶中),不需要在前面多加一个标记。

(注关于这里为什么不会把op_1这样的操作码当成是数据长度描述,因为这些操作码都是有一定的范围值,如大于某个值解析器不会把它当成push 多少字节,而是认为它是操作码,这一点将在我们了解解析器的具体逻辑时就会明白)

2.重载<<操作码

好,我们知道,除了<<吃入常用类型外,他还支持操作码,所以也有一定有个<<操作码类型的重载如下(吃入操作码):

cpp 复制代码
    CScript& operator<<(opcodetype opcode)
    {
        if (opcode <= OP_SINGLEBYTE_END)
        {
            insert(end(), (unsigned char)opcode);
        }
        else
        {
            assert(opcode >= OP_DOUBLEBYTE_BEGIN);
            insert(end(), (unsigned char)(opcode >> 8));
            insert(end(), (unsigned char)(opcode & 0xFF));
        }
        return (*this);
    }

3.opcodetype

opcodetype是个枚举类型,定义如下:

cpp 复制代码
enum opcodetype
{
    // push value
    OP_0 = 0,
    OP_FALSE = OP_0,
    OP_PUSHDATA1 = 76,
    OP_PUSHDATA2,
    OP_PUSHDATA4,
    OP_1NEGATE,
    OP_RESERVED,
    OP_1,
    OP_TRUE = OP_1,
    OP_2,
    OP_3,
    OP_4,
    OP_5,
    OP_6,
    OP_7,
    OP_8,
    OP_9,
    OP_10,
    OP_11,
    OP_12,
    OP_13,
    OP_14,
    OP_15,
    OP_16,

    // control
    OP_NOP,
    OP_VER,
    OP_IF,
    OP_NOTIF,
    OP_VERIF,
    OP_VERNOTIF,
    OP_ELSE,
    OP_ENDIF,
    OP_VERIFY,
    OP_RETURN,

    // stack ops
    OP_TOALTSTACK,
    OP_FROMALTSTACK,
    OP_2DROP,
    OP_2DUP,
    OP_3DUP,
    OP_2OVER,
    OP_2ROT,
    OP_2SWAP,
    OP_IFDUP,
    OP_DEPTH,
    OP_DROP,
    OP_DUP,
    OP_NIP,
    OP_OVER,
    OP_PICK,
    OP_ROLL,
    OP_ROT,
    OP_SWAP,
    OP_TUCK,

    // splice ops
    OP_CAT,
    OP_SUBSTR,
    OP_LEFT,
    OP_RIGHT,
    OP_SIZE,

    // bit logic
    OP_INVERT,
    OP_AND,
    OP_OR,
    OP_XOR,
    OP_EQUAL,
    OP_EQUALVERIFY,
    OP_RESERVED1,
    OP_RESERVED2,

    // numeric
    OP_1ADD,
    OP_1SUB,
    OP_2MUL,
    OP_2DIV,
    OP_NEGATE,
    OP_ABS,
    OP_NOT,
    OP_0NOTEQUAL,

    OP_ADD,
    OP_SUB,
    OP_MUL,
    OP_DIV,
    OP_MOD,
    OP_LSHIFT,
    OP_RSHIFT,

    OP_BOOLAND,
    OP_BOOLOR,
    OP_NUMEQUAL,
    OP_NUMEQUALVERIFY,
    OP_NUMNOTEQUAL,
    OP_LESSTHAN,
    OP_GREATERTHAN,
    OP_LESSTHANOREQUAL,
    OP_GREATERTHANOREQUAL,
    OP_MIN,
    OP_MAX,

    OP_WITHIN,

    // crypto
    OP_RIPEMD160,
    OP_SHA1,
    OP_SHA256,
    OP_HASH160,
    OP_HASH256,
    OP_CODESEPARATOR,
    OP_CHECKSIG,
    OP_CHECKSIGVERIFY,
    OP_CHECKMULTISIG,
    OP_CHECKMULTISIGVERIFY,


    // multi-byte opcodes
    OP_SINGLEBYTE_END = 0xF0,
    OP_DOUBLEBYTE_BEGIN = 0xF000,

    // template matching params
    OP_PUBKEY,
    OP_PUBKEYHASH,



    OP_INVALIDOPCODE = 0xFFFF,
};

以上都是操作码,关于每个操作码的意思,后续如有用到将做说明。

4.EvalScript

然后我们来看EvalScript函数是如何执行脚本的,先1个字节的读取数据,如下:

cpp 复制代码
bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType,
    vector<vector<unsigned char> >* pvStackRet)
{
    CAutoBN_CTX pctx;
    CScript::const_iterator pc = script.begin();
    CScript::const_iterator pend = script.end();
    CScript::const_iterator pbegincodehash = script.begin();
    vector<bool> vfExec;
    vector<valtype> stack;
    vector<valtype> altstack;
    if (pvStackRet)
        pvStackRet->clear();


    while (pc < pend)
    {

先获取第一个字节的迭代器pc,和最后一个字节的迭代器pend;

然后循环语句pc<pend里,以1个字节为单位的读取,直到遍历完。

5.GetOp

我们来看一个关键的函数:

cpp 复制代码
        bool fExec = !count(vfExec.begin(), vfExec.end(), false);

        //
        // Read instruction
        //
        opcodetype opcode;
        valtype vchPushValue;
        if (!script.GetOp(pc, opcode, vchPushValue))
            return false;

这里的GetOp是获取操作码,而现在pc指向第一个字节,也就是说,脚本的第一字节须是操作码或者数据标记(推送多少数据),而不是数据起始位置。这样包括循环后的pc,也遵循上述原则。

比如说要压入多少数据后,pc就会累加多少从而指向下一个操作码。这样循环调用GetOp就不会有问题。而不会出现数据干扰。

我们来看一下这个函数的代码和逻辑:

cpp 复制代码
 bool GetOp(iterator& pc, opcodetype& opcodeRet, vector<unsigned char>& vchRet)
 {
     // This is why people hate C++
     const_iterator pc2 = pc;
     bool fRet = GetOp(pc2, opcodeRet, vchRet);
     pc = begin() + (pc2 - begin());
     return fRet;
 }

 bool GetOp(const_iterator& pc, opcodetype& opcodeRet, vector<unsigned char>& vchRet) const
 {
     opcodeRet = OP_INVALIDOPCODE;
     vchRet.clear();
     if (pc >= end())
         return false;

     // Read instruction
     unsigned int opcode = *pc++;
     if (opcode >= OP_SINGLEBYTE_END)
     {
         if (pc + 1 > end())
             return false;
         opcode <<= 8;
         opcode |= *pc++;
     }

     // Immediate operand
     if (opcode <= OP_PUSHDATA4)
     {
         unsigned int nSize = opcode;
         if (opcode == OP_PUSHDATA1)
         {
             if (pc + 1 > end())
                 return false;
             nSize = *pc++;
         }
         else if (opcode == OP_PUSHDATA2)
         {
             if (pc + 2 > end())
                 return false;
             nSize = 0;
             memcpy(&nSize, &pc[0], 2);
             pc += 2;
         }
         else if (opcode == OP_PUSHDATA4)
         {
             if (pc + 4 > end())
                 return false;
             memcpy(&nSize, &pc[0], 4);
             pc += 4;
         }
         if (pc + nSize > end())
             return false;
         vchRet.assign(pc, pc + nSize);
         pc += nSize;
     }

     opcodeRet = (opcodetype)opcode;
     return true;
 }

我们注意到GetOp有两个版本,关键点在于pc参数,一个是非const一个是const版本,在非const版本中,定义了 const_iterator pc2 = pc;,然后再调用const版本,所以实际干活的是const版本。

而在EvalScript中也是定义了const pc,调用的是const版本,所以我们这里主要了解const版本的代码就行。

我们先来看三个参数,pc这个就是要读取的脚本了,传入的数据了,指向操作码(或标记)的迭代器。

opcodeRet,返回解析出来的迭代器,如op_1,op_add等。

vchRet 用来接收push的数据,如果有,比如是push指令,则这里跟着的是要push的数据。

另外GetOp里也负责维护pc的指针,它会根据情况将pc移动到下一条操作码。

6.清空初始化

我们来看开头的代码:

cpp 复制代码
   opcodeRet = OP_INVALIDOPCODE;
   vchRet.clear();
   if (pc >= end())
       return false;

就是清空一下数据,防止被之前的数据污染,op_invalid...一个特殊的值,无效的意思,无效操作码,你可以看作赋null之类的含义。

然后判断一下要求之类的,如果pc>=end说明读不了。已经超过了end.

接下来正式读取字节:

cpp 复制代码
// Read instruction
unsigned int opcode = *pc++;

把它赋予给opcode,并将pc后移一位,注意这里是pc++而不是++pc,所以是先赋值再自增。

  1. OP_SINGLEBYTE_END

然后开始解析这个opcode:

cpp 复制代码
    // Read instruction
    unsigned int opcode = *pc++;
    if (opcode >= OP_SINGLEBYTE_END)
    {
        if (pc + 1 > end())
            return false;
        opcode <<= 8;
        opcode |= *pc++;
    }

这里是判断,这个操作码是单字节操作码还是双字节操作码,这是为后来扩展而设计,为后续增加操作码,基本上可以无视。几乎不会被触发。原理是这样的,OP_SINGLEBYTE_END是单字节操作码的范围,如果第一个字节大于这个范围,那么说明是双字节操作码,那么把第一个字节左移8位,然后再加上第二个字节(因为是双字节操作码)。注意这里的opcode是unsinged int类型,虽然它接收的是一个字节的操作码。所以左移8位,第一个字节的数据并不会丢失。

8.OP_PUSHDATA

然后我们来看这个判断:

cpp 复制代码
  // Immediate operand
  if (opcode <= OP_PUSHDATA4)
  {

为什么会有OP_PUSHDATA这些操作码呢?

还记得之前的这段话吗:

(注关于这里为什么不会把op_1这样的操作码当成是数据长度描述,因为这些操作码都是有一定的范围值,如大于某个值解析器不会把它当成push多少字节)

这里的OP_PUSHDATA4就是那个临界值,只要小于这个OP_PUSHDATA4,就会把它解析为push操作。我们来看下这个OP_PUSHDATA4是怎么被定义的赋值的:

cpp 复制代码
enum opcodetype
{
    // push value
    OP_0 = 0,
    OP_FALSE = OP_0,
    OP_PUSHDATA1 = 76,
    OP_PUSHDATA2,
    OP_PUSHDATA4,
    OP_1NEGATE,
    OP_RESERVED,
    OP_1,
    OP_TRUE = OP_1,
    OP_2,
    OP_3,
    OP_4,
    OP_5,
    OP_6,
    OP_7,
    OP_8,
    OP_9,
    OP_10,
    OP_11,
    OP_12,
    OP_13,
    OP_14,
    OP_15,
    OP_16,

    // control
    OP_NOP,
    OP_VER,
    OP_IF,
    OP_NOTIF,
    OP_VERIF,

关键的看这里:

cpp 复制代码
enum opcodetype
{
    // push value
    OP_0 = 0,
    OP_FALSE = OP_0,
    OP_PUSHDATA1 = 76,
    OP_PUSHDATA2,
    OP_PUSHDATA4,

OP_PUSHDATA1=76,我们知道enum类型默认赋值是从0开始依次递增的。如果显式指定,那么下一个将会在赋值的基础上递增。所以OP_PUSHDATA2为77,OP_PUSHDATA4为78。而OP_1,OP_ADD这样的操作码肯定是大于OP_PUSHDATA4的。因为它们排在后面(在编译器里,鼠标停留在枚举项上,会显示出值来)。

所以当opcode小于等于OP_PUSHDATA4那么它就不是操作码(常规),这就是原理。

但是它是怎么指定push多少字节的数据呢?看到后缀的数字吗?OP_PUSHDATA1,表示这个操作码后面的1个字节,用来描述数据长度。2则表示2个字节,4则表示4个字节。

而如果是小于OP_PUSHDATA1=76,则直接表示数据长度。也就是说,当后面要push的数据小于76个字节时,则可以用数字直接表示,比如32个字节,就写32可以了,而不是OP_PUSHDATA1+32.

事实上这里没写出来,你可以看低于76的数值,都可以理解成默认的push的操作码,比如op_push1=1,push一个字节,op_push2=2,push 两个字节,虽然这里没显示定义,但解析时就是这么理解的。

而超过了76字节(包括),那么就得用OP_PUSHDATA1的方法,依此类推,超过255字节,就得用OP_PUSHDATA2的方法。

9.数据大小读取

明白了上面,那么我们再来看这三个判断,就非常好理解了:

cpp 复制代码
         unsigned int nSize = opcode;
         if (opcode == OP_PUSHDATA1)
         {
             if (pc + 1 > end())
                 return false;
             nSize = *pc++;
         }
         else if (opcode == OP_PUSHDATA2)
         {
             if (pc + 2 > end())
                 return false;
             nSize = 0;
             memcpy(&nSize, &pc[0], 2);
             pc += 2;
         }
         else if (opcode == OP_PUSHDATA4)
         {
             if (pc + 4 > end())
                 return false;
             memcpy(&nSize, &pc[0], 4);
             pc += 4;
         }

如果是pushdata1,则读后面的1个字节,用来描述大小。如果是pushdata2则读两个字节.....

10.读取数据

得到了数据大小,接下来根据这个大小,就是读取对应的数据了:

cpp 复制代码
    if (pc + nSize > end())
        return false;
    vchRet.assign(pc, pc + nSize);
    pc += nSize;

先判断一下大小逻辑检错问题,无意外,则调用assign拷贝数据,将pc到pc+nSize这个数据读到vchRet里,然后位移pc指针。

11.opCodeRet

然后就是转换一下操作码,返还回去,因为这里的opcode只是数值类型,将它转换为enum类型:

cpp 复制代码
   opcodeRet = (opcodetype)opcode;
   return true;

好,现在我们回到EvalScript函数。

执行完GetOp后:

12.if流程控制

cpp 复制代码
        bool fExec = !count(vfExec.begin(), vfExec.end(), false);

        //
        // Read instruction
        //
        opcodetype opcode;
        valtype vchPushValue;
        if (!script.GetOp(pc, opcode, vchPushValue))
            return false;

        if (fExec && opcode <= OP_PUSHDATA4)
            stack.push_back(vchPushValue);
        else if (fExec || (OP_IF <= opcode && opcode <= OP_ENDIF))

这段代码是什么意思呢?我们可以看到这段代码有个fExec参数深度参与其中,并且还有OP_IF和OP_ENDIF之类的判断,想理解这部分代码,我们必须先得了解脚本的if使用方法,即如何写脚本判断语句,我们先来看OP_IF操作码,OP_IF判断的是栈顶的值,然后根据真和假执行相应的语句流程写法如下:

cpp 复制代码
OP_0  //测试条件
OP_IF
   执行语句  (为真时执行)
OP_ELSE    (否则)
   执行语句
OP_ENDIF (结束)

我们把之前的例子改一下用来测试脚本,如下:

cpp 复制代码
int main()
{
    printf("Bitcoin v0.1.0 Simple Script Test\n");
    printf("Script: 2 3 OP_ADD 5 OP_EQUAL\n\n");

    // 1. 构造脚本
    CScript script;
    script << OP_0;//栈顶压入0
    script << OP_IF << 3 << 5 << OP_ADD;//如果为真则执行3+5
    script << OP_ELSE << 1 << 9 << OP_ADD;//如果为假则执行1+9
    script << OP_ENDIF;

    // 2. 执行脚本
    vector<vector<unsigned char> > stack;
    bool success = EvalScript(script, CTransaction(), 0, 0, &stack);


    if (success)
    {
        printf("EvalScript executed successfully!\n");

        if (!stack.empty())
        {
            const std::vector<unsigned char>& top = stack.back();
            bool finalResult = (top.size() > 0 && top[0] != 0);
            printf("Final stack size : %zu\n", stack.size());
            printf("Top of stack (bool): %s\n", finalResult ? "TRUE" : "FALSE");

            if (finalResult)
                printf("Script result: PASSED (as expected)\n");
            else
                printf("Script result: FAILED\n");
        }
        else
        {
            printf("Stack is empty!\n");
        }
    }
    else
    {
        printf("EvalScript failed!\n");
    }

    // 可选:打印栈中所有元素(调试用)
    printf("\nStack content (bottom to top):\n");
    for (size_t i = 0; i < stack.size(); ++i)
    {
        // 简单打印前几个字节
        printf("[%zu] size=%zu  ", i, stack[i].size());
        for (size_t j = 0; j < stack[i].size() && j < 8; ++j)
            printf("结果值:%d ", stack[i][j]);
        printf("\n");
    }

    return 0;
}

结果为10:

说明执行了else语句,逻辑正常,如果将OP_0改为OP_1,则会输出8,我实际测试正常,这里就不贴结果了。

好,但这里我是直接给个结果压入栈顶的,如果要判断3>5这样比大小的脚本,应该怎么写呢?

这里就留待下一章说明吧,今天就到这里。

相关推荐
信徒_6 小时前
做市商概念
大数据·区块链
2601_959480157 小时前
Moneta Markets亿汇:“比特币下探考验风险偏好”
区块链
CTA量化套保8 小时前
量化程序 while True 一直跑 CPU 很高:天勤降频与字段过滤
python·区块链
码云骑士8 小时前
ImToken智能合约交互避坑指南
区块链·智能合约·交互
开源量化GO13 小时前
多合约期货策略目标持仓怎么更新不容易乱
python·区块链
品牌数字化增长观察13 小时前
2026酒企组织效能的数字化提升实战解读
区块链·数字化·数实融合
MicroTech202514 小时前
量子威胁倒逼区块链安全革新,微算法科技(MLGO)量子原生区块链技术突破
科技·安全·区块链
Shota Kishi1 天前
SLV 新增 allnodes-jito 支持:Solana 验证者多客户端运维与 AI agent 自动化解析
运维·自动化·区块链
区块链小八歌1 天前
Berachain ERA:公链排放资本化与链上增长融资的范式革命
人工智能·区块链