ethereum/EIPs-155 Simple replay attack protection 35,36

关浩壤
2023-12-01

EIP 155:重放攻击保护——防止了在一个以太坊链上的交易被重复广播到另外一条链。

在看椭圆曲线时有提到,与r、s、v中的v相关

 

不同的共有链定义不同的chainId, 防止同一笔交易在不同的共有链上进行两次交易,防止重放攻击(其实就是防止测试网中的代币发送到主网中去)

v = 35 + chainId *2 | v = 36 + chainId * 2 (奇偶校验)

 

在创世区块的json文件中有设置:

"config": {
        "chainId": 555,
        "homesteadBlock": 0,
        "eip155Block": 0,//homesteadeip155两种版本都不是,那么就是Frontier版本了,所以v的值为27或28,看下面代码解释
        "eip158Block": 0
    },

参数代表的含义如下所示:

1、homesteadBlock:代表以太坊版本,这里我们设置为0。

2、eip155Block:我们的区块链并不涉及EIP155硬分叉(hard-fork),因此这个值设置为0。(这就是这个EIP将的内容)

3、eip158Block:同理,我们的区块链并不涉及EIP158硬分叉(hard-fork),因此这个值设置为0。

4.正在公开使用的chainId:

0: Olympic, Ethereum public pre-release testnet
1: Frontier, Homestead, Metropolis, the Ethereum public main network
1: Classic, the (un)forked public Ethereum Classic main network, chain ID 61
1: Expanse, an alternative Ethereum implementation, chain ID 2
2: Morden, the public Ethereum testnet, now Ethereum Classic testnet
3: Ropsten, the public cross-client Ethereum testnet
4: Rinkeby, the public Geth PoA testnet
8: Ubiq, the public Gubiq main network with flux difficulty chain ID 8
42: Kovan, the public Parity PoA testnet
77: Sokol, the public POA Network testnet
99: Core, the public POA Network main network
7762959: Musicoin, the music blockchain
61717561: Aquachain, ASIC resistant chain
[Other]: Could indicate that your connected to a local development test network.

注意:这里Classic是当年硬分叉后的ETC(叫以太经典),其的chainId为61(DAO(Decentralized Autonomous Organization)去中心化的自治组织);

2,3,4都为测试网络

EIP-155是后向兼容的,向后兼容是指旧软件所产生的数据或者代码可以被新软件使用,即win13能够使用win10的应用

反之,向前兼容即win10能使用win13的应用

Be aware that this backwards compatibility also means that transactions created from alternative Ethereum based blockchains that have not implemented EIP 155 (such as Ethereum Classic) can still be replayed on the main Ethereum chain.

 

eiptitleauthortypecategorystatuscreated
155
Simple replay attack protection
Vitalik Buterin
Standards Track
Core
Final
2016-10-14

Hard fork

Spurious Dragon

Parameters

  • FORK_BLKNUM: 2,675,000 (the DAO 事件后进行分叉的区块数)
  • CHAIN_ID: 1 (main net)

Specification

If block.number >= FORK_BLKNUM and v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36, then when computing the hash of a transaction for purposes of signing or recovering, instead of hashing only the first six elements (i.e. nonce, gasprice, startgas, to, value, data), hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0. The currently existing signature scheme using v = 27 and v = 28 remains valid and continues to operate under the same rules as it does now.

如果区块数大于分叉区块数并且v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36,那么当为了签名或恢复进行交易hash的计算时,不再仅仅只hash前六个元素,而是(nonce, gasprice, startgas, to, value, data),而是hash九个元素(还要再加上CHAIN_ID,r=0,s=0)。目前存在的使用 v = 27 and v = 28 来保证有效的签名方案将在现在使用的相同规则下继续操作(意思是这个也还在使用)v = 27 and v = 28说明的是版本(当其在签名中值为0x00或0x01时,要反向得到加密账户,即调用ecrecover函数时,要加27)

Example举例说明

Consider a transaction with nonce = 9, gasprice = 20 * 10**9, startgas = 21000, to = 0x3535353535353535353535353535353535353535, value = 10**18, data='' (empty).

The "signing data" becomes:

0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080

The "signing hash" becomes:

0xdaf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53

If the transaction is signed with the private key 0x4646464646464646464646464646464646464646464646464646464646464646, then the v,r,s values become:

(37, 18515461264373351373200002665853028612451056578545711640558177340181847433846, 46948507304638947509940763649030358759909902576025900602547168820602576006531)

Notice the use of 37 instead of 27. The signed tx would become:

0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83

Rationale

This would provide a way to send transactions that work on Ethereum without working on ETC or the Morden testnet. ETC is encouraged to adopt this EIP but replacing CHAIN_ID with a different value, and all future testnets, consortium chains and alt-etherea are encouraged to adopt this EIP replacing CHAIN_ID with a unique value.

这样子就提供了一种方法来保证交易是使用在了以太坊上而不是另一个分叉ETC或测试网络Morden上,主网上的v = 1*2 + 35 = 37 或 v = 1*2 + 36 = 38(注意:这也是为什么之前在查看一些现在生成的签名的v的时候,发现他们的v并不等于27或28,这个后面要注意一下,为什么还是有些是27和28,这是看你使用的版本的原因,下面有解释)

 

List of Chain ID's:

CHAIN_IDChain(s)
1Ethereum mainnet
2Morden (disused), Expanse mainnet
3Ropsten
4Rinkeby
30Rootstock mainnet
31Rootstock testnet
42Kovan
61Ethereum Classic mainnet
62Ethereum Classic testnet
1337Geth private chains (default)

签名只会生成r,s两个的值,v是加上去的

举个例子说明:

web3.eth.signTransaction({
    from: "0xEB014f8c8B418Db6b45774c326A0E64C78914dC0",
    gasPrice: "20000000000",
    gas: "21000",
    to: '0x3535353535353535353535353535353535353535',
    value: "1000000000000000000",
    data: ""
}).then(console.log);
> {
    raw: '0xf86c808504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a04f4c17305743700648bc4f6cd3038ec6f6af0df73e31757007b7f59df7bee88da07e1941b264348e80c78c4027afc65a87b0a5e43e86742b8ca0823584c6788fd0',
    tx: {
        nonce: '0x0',
        gasPrice: '0x4a817c800',
        gas: '0x5208',
        to: '0x3535353535353535353535353535353535353535',
        value: '0xde0b6b3a7640000',
        input: '0x',
        v: '0x25',
        r: '0x4f4c17305743700648bc4f6cd3038ec6f6af0df73e31757007b7f59df7bee88d',
        s: '0x7e1941b264348e80c78c4027afc65a87b0a5e43e86742b8ca0823584c6788fd0',
        hash: '0xda3be87732110de6c1354c83770aae630ede9ac308d9f7b399ecfba23d923384'
    }
}

将raw处的签名分解为tx处的内容:

0xf86c8085 0 4a817c800 82 5208 94 3535353535353535353535353535353535353535 880 de0b6b3a7640000 80 25 a0 4f4c17305743700648bc4f6cd3038ec6f6af0df73e31757007b7f59df7bee88d a0 7e1941b264348e80c78c4027afc65a87b0a5e43e86742b8ca0823584c6788fd0

将这里的数据分开后,可以看见每一个数据所在的位置,但是很奇怪的地方就是为什么每个数据之间都有一些奇怪的数字,比如后面的v,r,s,中间都有着0xa0,这些值是有什么意义的吗,希望能找到解释

从这里的结果我们就能够看见这里的v = 0x25 = 37,说明它是在主网运行的

 

还有另一个例子:

var rawTx = {
        from: '0x91b678137f09c8b4f294a14e88c09276522618cf',
        nonce: '0x'+nonce,
        gasPrice: '0x09184e72a000',
        gasLimit: 3000000,//2dc6c0
        to: '0xceff99a34d9f6e7d3deae2bd0604086645368aee',
        value: '0x00',
        data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057'
    };

得到的签名分解是结果是:

f88a0b86 0 9184e72a000 83 2dc6c0 94 ceff99a34d9f6e7d3deae2bd0604086645368aee 80a4 7f7465737432000000000000000000000000000000000000000000000000000000600057 1b a0 b9c52b010ef238339f710bee60b8597b2953c19d32680490b8f0acc82f2fe3be a0 40f8e42202fdb3b72e2f2976fc5e4ae7485b45ca702b23792b38add2d4912ccb

可见这里的v = 0x1b = 27,因为这个结果是我在本地区块链运行的交易,并不涉及EIP155硬分叉(hard-fork),eip155Block这个值是设置为0的

 

下面由代码上查看:

https://github.com/ethereum/go-ethereum/blob/master/core/types/transaction_signing.go

从代码上我们可以看见,当交易实现的规则不同时,v的值也会相应不同

1.EIP155Signer,则 v = chainId *2 +35,说明使用的是奇偶校验中的偶校验,即曲线点为偶数(如果用的是36,则是奇校验)

2.HomesteadSigner,v is 0 or 1 (0为偶检验,1为奇校验)

3.FrontierSigner,为27 or 28 (27为偶检验,28为奇校验)

go-ethereum/core/types/transaction_signing.go

// EIP155Transaction implements Signer using the EIP155 rules.
type EIP155Signer struct {
    chainId, chainIdMul *big.Int
}

func NewEIP155Signer(chainId *big.Int) EIP155Signer {
    if chainId == nil {
        chainId = new(big.Int)
    }
    return EIP155Signer{
        chainId:    chainId,
        chainIdMul: new(big.Int).Mul(chainId, big.NewInt(2)),
    }
}

func (s EIP155Signer) Equal(s2 Signer) bool {
    eip155, ok := s2.(EIP155Signer)
    return ok && eip155.chainId.Cmp(s.chainId) == 0
}

var big8 = big.NewInt(8)

func (s EIP155Signer) Sender(tx *Transaction) (common.Address, error) {
    if !tx.Protected() {
        return HomesteadSigner{}.Sender(tx)
    }
    if tx.ChainId().Cmp(s.chainId) != 0 {
        return common.Address{}, ErrInvalidChainId
    }
    V := new(big.Int).Sub(tx.data.V, s.chainIdMul)
    V.Sub(V, big8)
    return recoverPlain(s.Hash(tx), tx.data.R, tx.data.S, V, true)
}

// WithSignature returns a new transaction with the given signature. This signature
// needs to be in the [R || S || V] format where V is 0 or 1.
func (s EIP155Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) {
    R, S, V, err = HomesteadSigner{}.SignatureValues(tx, sig)
    if err != nil {
        return nil, nil, nil, err
    }
    if s.chainId.Sign() != 0 {
        V = big.NewInt(int64(sig[64] + 35))
        V.Add(V, s.chainIdMul)
    }
    return R, S, V, nil
}

// Hash returns the hash to be signed by the sender.
// It does not uniquely identify the transaction.
func (s EIP155Signer) Hash(tx *Transaction) common.Hash {
    return rlpHash([]interface{}{
        tx.data.AccountNonce,
        tx.data.Price,
        tx.data.GasLimit,
        tx.data.Recipient,
        tx.data.Amount,
        tx.data.Payload,
        s.chainId, uint(0), uint(0),
    })
}

// HomesteadTransaction implements TransactionInterface using the
// homestead rules.
type HomesteadSigner struct{ FrontierSigner }

func (s HomesteadSigner) Equal(s2 Signer) bool {
    _, ok := s2.(HomesteadSigner)
    return ok
}

// SignatureValues returns signature values. This signature
// needs to be in the [R || S || V] format where V is 0 or 1.
func (hs HomesteadSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
    return hs.FrontierSigner.SignatureValues(tx, sig)
}

func (hs HomesteadSigner) Sender(tx *Transaction) (common.Address, error) {
    return recoverPlain(hs.Hash(tx), tx.data.R, tx.data.S, tx.data.V, true)
}

type FrontierSigner struct{}

func (s FrontierSigner) Equal(s2 Signer) bool {
    _, ok := s2.(FrontierSigner)
    return ok
}

// SignatureValues returns signature values. This signature
// needs to be in the [R || S || V] format where V is 0 or 1.
func (fs FrontierSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
    if len(sig) != 65 {
        panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
    }
    r = new(big.Int).SetBytes(sig[:32])
    s = new(big.Int).SetBytes(sig[32:64])
    v = new(big.Int).SetBytes([]byte{sig[64] + 27})
    return r, s, v, nil
}

// Hash returns the hash to be signed by the sender.
// It does not uniquely identify the transaction.
func (fs FrontierSigner) Hash(tx *Transaction) common.Hash {
    return rlpHash([]interface{}{
        tx.data.AccountNonce,
        tx.data.Price,
        tx.data.GasLimit,
        tx.data.Recipient,
        tx.data.Amount,
        tx.data.Payload,
    })
}

func (fs FrontierSigner) Sender(tx *Transaction) (common.Address, error) {
    return recoverPlain(fs.Hash(tx), tx.data.R, tx.data.S, tx.data.V, false)
}

func recoverPlain(sighash common.Hash, R, S, Vb *big.Int, homestead bool) (common.Address, error) {
    if Vb.BitLen() > 8 {
        return common.Address{}, ErrInvalidSig
    }
    V := byte(Vb.Uint64() - 27)
    if !crypto.ValidateSignatureValues(V, R, S, homestead) {
        return common.Address{}, ErrInvalidSig
    }
    // encode the snature in uncompressed format
    r, s := R.Bytes(), S.Bytes()
    sig := make([]byte, 65)
    copy(sig[32-len(r):32], r)
    copy(sig[64-len(s):64], s)
    sig[64] = V
    // recover the public key from the snature
    pub, err := crypto.Ecrecover(sighash[:], sig)
    if err != nil {
        return common.Address{}, err
    }
    if len(pub) == 0 || pub[0] != 4 {
        return common.Address{}, errors.New("invalid public key")
    }
    var addr common.Address
    copy(addr[:], crypto.Keccak256(pub[1:])[12:])
    return addr, nil
}

 

通过上面的r,s,v得到的签名和信息hash来恢复公钥:

https://github.com/ethereum/go-ethereum/blob/master/crypto/signature_cgo.go

go-ethereum/crypto/signature_cgo.go

// Ecrecover returns the uncompressed public key that created the given signature.
func Ecrecover(hash, sig []byte) ([]byte, error) {
    return secp256k1.RecoverPubkey(hash, sig)
}

 

https://github.com/ethereum/go-ethereum/blob/master/crypto/secp256k1/secp256.go

go-ethereum/crypto/secp256k1/secp256.go

// RecoverPubkey returns the public key of the signer.
// msg must be the 32-byte hash of the message to be signed.
// sig must be a 65-byte compact ECDSA signature containing the recovery id as the last element.
func RecoverPubkey(msg []byte, sig []byte) ([]byte, error) {
    if len(msg) != 32 {
        return nil, ErrInvalidMsgLen
    }
    if err := checkSignature(sig); err != nil {
        return nil, err
    }

    var (
        pubkey  = make([]byte, 65)
        sigdata = (*C.uchar)(unsafe.Pointer(&sig[0]))
        msgdata = (*C.uchar)(unsafe.Pointer(&msg[0]))
    )
    if C.secp256k1_ext_ecdsa_recover(context, (*C.uchar)(unsafe.Pointer(&pubkey[0])), sigdata, msgdata) == 0 {
        return nil, ErrRecoverFailed
    }
    return pubkey, nil
}

 

https://github.com/ethereum/go-ethereum/blob/master/crypto/secp256k1/libsecp256k1/src/modules/recovery/main_impl.h

go-ethereum/crypto/secp256k1/libsecp256k1/src/modules/recovery/main_impl.h

int secp256k1_ecdsa_recover(const secp256k1_context* ctx, secp256k1_pubkey *pubkey, const secp256k1_ecdsa_recoverable_signature *signature, const unsigned char *msg32) {
    secp256k1_ge q;
    secp256k1_scalar r, s;
    secp256k1_scalar m;
    int recid;
    VERIFY_CHECK(ctx != NULL);
    ARG_CHECK(secp256k1_ecmult_context_is_built(&ctx->ecmult_ctx));
    ARG_CHECK(msg32 != NULL);
    ARG_CHECK(signature != NULL);
    ARG_CHECK(pubkey != NULL);

    secp256k1_ecdsa_recoverable_signature_load(ctx, &r, &s, &recid, signature);
    VERIFY_CHECK(recid >= 0 && recid < 4);  /* should have been caught in parse_compact */说明v可以为0,1,2,3
    secp256k1_scalar_set_b32(&m, msg32, NULL);
    if (secp256k1_ecdsa_sig_recover(&ctx->ecmult_ctx, &r, &s, &q, &m, recid)) {
        secp256k1_pubkey_save(pubkey, &q);
        return 1;
    } else {
        memset(pubkey, 0, sizeof(*pubkey));
        return 0;
    }
}

同一个代码下

/** Group order for secp256k1 defined as 'n' in "Standards for Efficient Cryptography" (SEC2) 2.7.1
 *  sage: for t in xrange(1023, -1, -1):
 *     ..   p = 2**256 - 2**32 - t
 *     ..   if p.is_prime():
 *     ..     print '%x'%p
 *     ..     break
 *   'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'
 *  sage: a = 0
 *  sage: b = 7
 *  sage: F = FiniteField (p)
 *  sage: '%x' % (EllipticCurve ([F (a), F (b)]).order())
 *   'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'
 */
static const secp256k1_fe_t secp256k1_ecdsa_const_order_as_fe = SECP256K1_FE_CONST(//这是私钥的最大值
    0xFFFFFFFFUL, 0xFFFFFFFFUL, 0xFFFFFFFFUL, 0xFFFFFFFEUL,
    0xBAAEDCE6UL, 0xAF48A03BUL, 0xBFD25E8CUL, 0xD0364141UL
);

/** Difference between field and order, values 'p' and 'n' values defined in
 *  "Standards for Efficient Cryptography" (SEC2) 2.7.1.
 *  sage: p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
 *  sage: a = 0
 *  sage: b = 7
 *  sage: F = FiniteField (p)
 *  sage: '%x' % (p - EllipticCurve ([F (a), F (b)]).order())
 *   '14551231950b75fc4402da1722fc9baee'
 */
static const secp256k1_fe_t secp256k1_ecdsa_const_p_minus_order = SECP256K1_FE_CONST(
    0, 0, 0, 1, 0x45512319UL, 0x50B75FC4UL, 0x402DA172UL, 0x2FC9BAEEUL
);

 

static int secp256k1_ecdsa_sig_recover(const secp256k1_ecmult_context *ctx, const secp256k1_scalar *sigr, const secp256k1_scalar* sigs, secp256k1_ge *pubkey, const secp256k1_scalar *message, int recid) {
    unsigned char brx[32];
    secp256k1_fe fx;
    secp256k1_ge x;
    secp256k1_gej xj;
    secp256k1_scalar rn, u1, u2;
    secp256k1_gej qj;
    int r;

    if (secp256k1_scalar_is_zero(sigr) || secp256k1_scalar_is_zero(sigs)) {
        return 0;
    }

    secp256k1_scalar_get_b32(brx, sigr);
    r = secp256k1_fe_set_b32(&fx, brx);
    (void)r;
    VERIFY_CHECK(r); /* brx comes from a scalar, so is less than the order; certainly less than p */
    if (recid & 2) {//用于获得v二进制倒数第二位的值,即当v = 2或3时,即二进制倒数第二位为1,才进入该判断语句
        if (secp256k1_fe_cmp_var(&fx, &secp256k1_ecdsa_const_p_minus_order) >= 0) {
       /* fx + p >= n, so we can skip testing the second case. */ return 0; } secp256k1_fe_add(&fx, &secp256k1_ecdsa_const_order_as_fe); }
//所以走到这的即v = 0或1的签名 if (!secp256k1_ge_set_xo_var(&x, &fx, recid & 1)) { return 0; }
//前面这部分的内容真的很复杂,但是看起来像是进行一些检验 secp256k1_gej_set_ge(&xj, &x); secp256k1_scalar_inverse_var(&rn, sigr); secp256k1_scalar_mul(&u1, &rn, message); secp256k1_scalar_negate(&u1, &u1); secp256k1_scalar_mul(&u2, &rn, sigs); secp256k1_ecmult(ctx, &qj, &xj, &u2, &u1); secp256k1_ge_set_gej_var(pubkey, &qj); return !secp256k1_gej_is_infinity(&qj); }

 

https://github.com/ethereum/go-ethereum/blob/461291882edce0ac4a28f64c4e8725b7f57cbeae/crypto/secp256k1/libsecp256k1/src/field_5x52_impl.h

go-ethereum/crypto/secp256k1/libsecp256k1/src/field_5x52_impl.h

static int secp256k1_fe_cmp_var(const secp256k1_fe *a, const secp256k1_fe *b) {
    int i;
#ifdef VERIFY
    VERIFY_CHECK(a->normalized);
    VERIFY_CHECK(b->normalized);
    secp256k1_fe_verify(a);
    secp256k1_fe_verify(b);
#endif
    for (i = 4; i >= 0; i--) {
        if (a->n[i] > b->n[i]) {
            return 1;
        }
        if (a->n[i] < b->n[i]) {
            return -1;
        }
    }
    return 0;
}

 

#ifdef VERIFY
static void secp256k1_fe_verify(const secp256k1_fe *a) {
    const uint64_t *d = a->n;
    int m = a->normalized ? 1 : 2 * a->magnitude, r = 1;
   /* secp256k1 'p' value defined in "Standards for Efficient Cryptography" (SEC2) 2.7.1. */
    r &= (d[0] <= 0xFFFFFFFFFFFFFULL * m);
    r &= (d[1] <= 0xFFFFFFFFFFFFFULL * m);
    r &= (d[2] <= 0xFFFFFFFFFFFFFULL * m);
    r &= (d[3] <= 0xFFFFFFFFFFFFFULL * m);
    r &= (d[4] <= 0x0FFFFFFFFFFFFULL * m);
    r &= (a->magnitude >= 0);
    r &= (a->magnitude <= 2048);
    if (a->normalized) {
        r &= (a->magnitude <= 1);
        if (r && (d[4] == 0x0FFFFFFFFFFFFULL) && ((d[3] & d[2] & d[1]) == 0xFFFFFFFFFFFFFULL)) {
            r &= (d[0] < 0xFFFFEFFFFFC2FULL);
        }
    }
    VERIFY_CHECK(r == 1);
}
#endif

 

SECP256K1_INLINE static void secp256k1_fe_add(secp256k1_fe *r, const secp256k1_fe *a) {
#ifdef VERIFY
    secp256k1_fe_verify(a);
#endif
    r->n[0] += a->n[0];
    r->n[1] += a->n[1];
    r->n[2] += a->n[2];
    r->n[3] += a->n[3];
    r->n[4] += a->n[4];
#ifdef VERIFY
    r->magnitude += a->magnitude;
    r->normalized = 0;
    secp256k1_fe_verify(r);
#endif
}

 

https://github.com/ethereum/go-ethereum/blob/461291882edce0ac4a28f64c4e8725b7f57cbeae/crypto/secp256k1/libsecp256k1/src/group_impl.h

go-ethereum/crypto/secp256k1/libsecp256k1/src/group_impl.h

static int secp256k1_ge_set_xo_var(secp256k1_ge *r, const secp256k1_fe *x, int odd) {
    if (!secp256k1_ge_set_xquad(r, x)) {
        return 0;
    }
    secp256k1_fe_normalize_var(&r->y);
    if (secp256k1_fe_is_odd(&r->y) != odd) {
        secp256k1_fe_negate(&r->y, &r->y, 1);
    }
    return 1;

}

上面的代码太复杂,没看懂,但是大概觉得v的作用应该是用来对r的值进行的检验,以后在慢慢看吧!!!!!!!!!!!!!

 

转载于:https://www.cnblogs.com/wanghui-garcia/p/9648147.html

 类似资料:

相关阅读

相关文章

相关问答