secp256k1的结构——公钥

2021SC@SDUSC


本文将介绍secp256k1定义的公钥结构,主要介绍公钥的三种格式以及公钥相应的函数实现。

公钥解析的格式

公钥的格式下面例举了三种,分别是Compressed、Full和Raw:

pub enum PublicKeyFormat {
    /// Compressed public key, 33 bytes.
    Compressed,
    /// Full length public key, 65 bytes.
    Full,
    /// Raw public key, 64 bytes.
    Raw,
}

下面分别介绍这个东西代表什么意思。
Compressed public key即为压缩公钥;
Full length public key即为未压缩公钥;
Raw public key即为原始公钥。
虽然上面是这么个顺序,但是介绍的话需要反过来介绍。

Raw public key原始公钥

先说Raw public key原始公钥,这其实就是最先认识的那个公钥,它用64字节即512比特表示,由于公钥点坐标的横纵坐标都是256位比特32字节,这个64字节的公钥其实就是把横纵坐标直接拼起来。

这样表示公钥的话省时省力,公钥是什么样就表示成为什么样。但是有一个用于序列化ECDSA的公钥标准,称之为Standard for Efficient Cryptography(SEC高效加密标准),那么既然他说高效了,直接原样存储公钥肯定不是最高效的,于是就有了未压缩公钥和压缩公钥。

Full length public key未压缩公钥

未压缩公钥有65个字节,比原始公钥还要多一个字节,这其实是为了区分未压缩公钥和压缩公钥,给他们分别加一个一字节的前缀。未压缩公钥的一字节前缀用16进制表示为0x04,由于是16进制表示,04代表两个16进制数,共8比特,即一字节。

Compressed public key压缩公钥

压缩公钥有33字节,和未压缩公钥一样,在压缩公约前面加了一个字节的前缀来表示是压缩后的公钥,剩下的32字节就只有公钥的x坐标,y坐标不再保留,要得到公钥的y坐标需要把x坐标代入到椭圆曲线方程中得到。
但是这里就会有一个问题,有限域椭圆曲线方程关于y=p/2对称,如果只给一个x坐标会得到两个y坐标值,且这两个y坐标值的和为p。这样就需要对具体是哪个y来标记,由于两个y坐标值的和为p,那么必定有一个y是奇数,另一个是偶数。规定一字节前缀如果为0x02则表示y为偶数,0x03表示y为奇数,这样就可以使用1字节前缀+32字节x坐标来表示压缩后的33字节公钥了。

公钥PublicKey

结构体PublicKey表示公钥,公钥就是一个仿射坐标点:

pub struct PublicKey(Affine);

公钥实现的函数如下图所示,相比Message而言多了很多,下文将会具体分析,这里先把细节抹掉,关注整体实现:

impl PublicKey {
    //公钥生成:
pub fn from_secret_key_with_context(seckey: &SecretKey, 
context: &ECMultGenContext,) -> PublicKey {
    ...
    }
#[cfg(any(feature = "static-context", feature = "lazy-static-context"))]
pub fn from_secret_key(seckey: &SecretKey) -> PublicKey {
       ...
    }
     //公钥反序列化
pub fn parse_slice(p: &[u8], format: Option<PublicKeyFormat>) -> Result<PublicKey, Error> {
...
    }

pub fn parse(p: &[u8; util::FULL_PUBLIC_KEY_SIZE]) -> Result<PublicKey, Error> {...
    }
     
pub fn parse_compressed(
        p: &[u8; util::COMPRESSED_PUBLIC_KEY_SIZE],
    ) -> Result<PublicKey, Error> {
        ...
    }
     //公钥序列化
pub fn serialize(&self) -> [u8; util::FULL_PUBLIC_KEY_SIZE] {
    ...
    }

pub fn serialize_compressed(&self) -> [u8; util::COMPRESSED_PUBLIC_KEY_SIZE] {
...
    }
}

下面开始讲述具体的函数实现:

公钥生成函数

公钥生成过程很简单,就是根据椭圆曲线群的生成元G,与私钥进行标量乘法运算,得到的坐标点就是公钥,这里就是公钥生成函数,实现上述过程。

这个函数表示根据context上下文生成公钥,这里的context上下文其实就是ECMultGenContext,即专门计算 a G aG aG类型运算的那个impl,ecmult_gen函数应该看到比较熟悉,在libsecp256k1比特币密码算法开源库(九)的ECMultGenContext部分介绍过这个函数,就是根据传入的Jacobian类型的坐标点pj和Scalar类型的标量seckey进行标量乘法运算,得到结果点坐标。由于最后结果点坐标还是Jacobian类型,需要得到的公钥是一个仿射坐标点,需要用set_gej函数将Jacobian坐标点转化为Affine仿射坐标点。

pub fn from_secret_key_with_context(
        seckey: &SecretKey,
        context: &ECMultGenContext,
    ) -> PublicKey {
        let mut pj = Jacobian::default();
        context.ecmult_gen(&mut pj, &seckey.0);
        let mut p = Affine::default();
        p.set_gej(&pj);
        PublicKey(p)
    }

set_gej函数实现如下代码所示,实现的功能就是根据传入的Jacobian类型坐标点转化为Affine仿射坐标点,之前博客中提到过,Jacobian类型坐标点转化为Affine仿射坐标点的对应关系为:Jacobian射影坐标点 ( x , y , z ) (x,y,z) (x,y,z)对应仿射坐标中的点 ( x z 2 , y z 3 ) (\frac {x}{z^2},\frac {y}{z^3}) (z2x,z3y),按照这个对应关系很容易看懂下面的代码,在相应位置我给出了注释。

pub fn set_gej(&mut self, a: &Jacobian) {
        self.infinity = a.infinity;
        let mut a = *a;
        a.z = a.z.inv();//对z求逆,相当于1/z
        let z2 = a.z.sqr();//对z逆求平方
        let z3 = a.z * z2;//对z逆求三次方
        a.x *= z2;//a.x即为x/z^2,即为仿射坐标横坐标点
        a.y *= z3;//a.y即为y/z^3,即为Jacobian射影坐标纵坐标点
        a.z.set_int(1);
        self.x = a.x;
        self.y = a.y;
    }

这个函数没啥好说的,就是把上面那个函数给封装了一下,直接调用上面的那个函数from_secret_key_with_context:

pub fn from_secret_key(seckey: &SecretKey) -> PublicKey {
        Self::from_secret_key_with_context(seckey, &ECMULT_GEN_CONTEXT)
    }

反序列化

这个函数运行实现的就是把一个序列化的公钥给反序列化,并加入了错误处理机制。具体的反序列化函数prase(处理未压缩公钥)和parse_compressed(处理压缩公钥)的实现在这段代码的后面给出了解释。
在本文最开始的部分介绍了原始公钥、压缩公钥和未压缩公钥,这段代码的在反序列化操作前先识别传入的公钥p是哪种类型,对原始公钥做得很绝,直接返回错误了,但是在后面又把原始公钥给转化成了未压缩公钥;对于压缩公钥和未压缩公钥都是直接反序列化为PublicKey,不报错。

pub fn parse_slice(p: &[u8], format: Option<PublicKeyFormat>) -> Result<PublicKey, Error> {
//识别公钥类型
        let format = match (p.len(), format) {
            (util::FULL_PUBLIC_KEY_SIZE, None)
            | (util::FULL_PUBLIC_KEY_SIZE, Some(PublicKeyFormat::Full)) => PublicKeyFormat::Full,
            (util::COMPRESSED_PUBLIC_KEY_SIZE, None)
            | (util::COMPRESSED_PUBLIC_KEY_SIZE, Some(PublicKeyFormat::Compressed)) => {
                PublicKeyFormat::Compressed
            }
            (util::RAW_PUBLIC_KEY_SIZE, None)
            | (util::RAW_PUBLIC_KEY_SIZE, Some(PublicKeyFormat::Raw)) => PublicKeyFormat::Raw,
            _ => return Err(Error::InvalidInputLength),//原始公钥返回异常
        };
//反序列化公钥
        match format {
        //未压缩公钥使用反序列化函数prase反序列化
            PublicKeyFormat::Full => {
                let mut a = [0; util::FULL_PUBLIC_KEY_SIZE];
                a.copy_from_slice(p);
                Self::parse(&a)
            }
            //将原始公钥转化为未压缩公钥并反序列化
            PublicKeyFormat::Raw => {
                use util::TAG_PUBKEY_FULL;

                let mut a = [0; util::FULL_PUBLIC_KEY_SIZE];
                a[0] = TAG_PUBKEY_FULL;
                a[1..].copy_from_slice(p);
                Self::parse(&a)
            }
             //压缩公钥使用反序列化函数parse_compressed反序列化
            PublicKeyFormat::Compressed => {
                let mut a = [0; util::COMPRESSED_PUBLIC_KEY_SIZE];
                a.copy_from_slice(p);
                Self::parse_compressed(&a)
            }
        }
    }

下面是专门针对未压缩公钥的反序列化函数prase实现过程,主要思路就是调用set_b32函数分别将x和y进行反序列化处理,其中在代码段
if !x.set_b32(array_ref!(p, 1, 32))
中,(p, 1, 32)表示在数组p中截取下标index为1-32的部分,其中1表示index下标,32表示偏移量offset,共32个数组元素,刚好表示x坐标;同样地在代码段
if !y.set_b32(array_ref!(p, 33, 32))
中,33表示index下标,32表示偏移量offset,即截取数组下标33-64的数组元素,刚好表示y坐标。

pub fn parse(p: &[u8; util::FULL_PUBLIC_KEY_SIZE]) -> Result<PublicKey, Error> {
        use util::{TAG_PUBKEY_FULL, TAG_PUBKEY_HYBRID_EVEN, TAG_PUBKEY_HYBRID_ODD};

        if !(p[0] == TAG_PUBKEY_FULL
            || p[0] == TAG_PUBKEY_HYBRID_EVEN
            || p[0] == TAG_PUBKEY_HYBRID_ODD)
        {
            return Err(Error::InvalidPublicKey);
        }
        let mut x = Field::default();
        let mut y = Field::default();
        //反序列化处理x坐标和y坐标
        if !x.set_b32(array_ref!(p, 1, 32)) {
            return Err(Error::InvalidPublicKey);
        }
        //将处理结束后的坐标转换为Affine类型的变量
        if !y.set_b32(array_ref!(p, 33, 32)) {
            return Err(Error::InvalidPublicKey);
        }
        let mut elem = Affine::default();
        elem.set_xy(&x, &y);
        if (p[0] == TAG_PUBKEY_HYBRID_EVEN || p[0] == TAG_PUBKEY_HYBRID_ODD)
            && (y.is_odd() != (p[0] == TAG_PUBKEY_HYBRID_ODD))
        {
            return Err(Error::InvalidPublicKey);
        }
        if elem.is_infinity() {
            return Err(Error::InvalidPublicKey);
        }
        if elem.is_valid_var() {
            Ok(PublicKey(elem))
        } else {
            Err(Error::InvalidPublicKey)
        }
    }

截取好对应部分后分别传入set_b32函数进行反序列化处理得到反序列化的x坐标和y坐标。注意这里的set_b32和Message那篇的set_b32虽然名字一样,但是实现不一样,这是因为Message实际上就是一个scalar(即标量),它只有256字节,因此需要8个u32数组元素;但是公钥是一个Field,在压缩前它有320字节(压缩存储之后才会变成256字节),因此需要10个u32数组元素接收。在下面的代码中你可以看到使用了 self.n[0] ~ self.n[9]共计10个数组元素。

#[must_use]
    pub fn set_b32(&mut self, a: &[u8; 32]) -> bool {
        self.n[0] = (a[31] as u32)
            | ((a[30] as u32) << 8)
            | ((a[29] as u32) << 16)
            | (((a[28] & 0x3) as u32) << 24);
        self.n[1] = (((a[28] >> 2) & 0x3f) as u32)
            | ((a[27] as u32) << 6)
            | ((a[26] as u32) << 14)
            | (((a[25] & 0xf) as u32) << 22);
        self.n[2] = (((a[25] >> 4) & 0xf) as u32)
            | ((a[24] as u32) << 4)
            | ((a[23] as u32) << 12)
            | (((a[22] as u32) & 0x3f) << 20);
        self.n[3] = (((a[22] >> 6) & 0x3) as u32)
            | ((a[21] as u32) << 2)
            | ((a[20] as u32) << 10)
            | ((a[19] as u32) << 18);
        self.n[4] = (a[18] as u32)
            | ((a[17] as u32) << 8)
            | ((a[16] as u32) << 16)
            | (((a[15] & 0x3) as u32) << 24);
        self.n[5] = (((a[15] >> 2) & 0x3f) as u32)
            | ((a[14] as u32) << 6)
            | ((a[13] as u32) << 14)
            | (((a[12] as u32) & 0xf) << 22);
        self.n[6] = (((a[12] >> 4) & 0xf) as u32)
            | ((a[11] as u32) << 4)
            | ((a[10] as u32) << 12)
            | (((a[9] & 0x3f) as u32) << 20);
        self.n[7] = (((a[9] >> 6) & 0x3) as u32)
            | ((a[8] as u32) << 2)
            | ((a[7] as u32) << 10)
            | ((a[6] as u32) << 18);
        self.n[8] = (a[5] as u32)
            | ((a[4] as u32) << 8)
            | ((a[3] as u32) << 16)
            | (((a[2] & 0x3) as u32) << 24);
        self.n[9] = (((a[2] >> 2) & 0x3f) as u32) | ((a[1] as u32) << 6) | ((a[0] as u32) << 14);

        if self.n[9] == 0x03fffff
            && (self.n[8] & self.n[7] & self.n[6] & self.n[5] & self.n[4] & self.n[3] & self.n[2])
                == 0x3ffffff
            && (self.n[1] + 0x40 + ((self.n[0] + 0x3d1) >> 26)) > 0x3ffffff
        {
            return false;
        }

        self.magnitude = 1;
        self.normalized = true;
        debug_assert!(self.verify());

        true
    }

最后创建一个Affine类型的变量,调用set_xy函数接收x坐标和y坐标,实现将序列化数组p转化为了一个Affine类型的变量elem,实现了反序列化的过程。

set_xy函数实现如下所示,即创建一个与给定x和y坐标对应的椭圆曲线群元素,且坐标为仿射坐标。

 pub fn set_xy(&mut self, x: &Field, y: &Field) {
        self.infinity = false;
        self.x = *x;
        self.y = *y;
    }

下面是专门针对压缩公钥的反序列化函数parse_compressed实现过程,压缩的公钥没有y坐标只有x坐标,因此只对x坐标使用set_b32函数处理即可,得到反序列化的x坐标。

最后创建一个Affine类型的变量,调用set_xo_var函数接收x坐标,实现将序列化数组p转化为了一个Affine类型的变量elem,实现了反序列化的过程。

pub fn parse_compressed(
        p: &[u8; util::COMPRESSED_PUBLIC_KEY_SIZE],
    ) -> Result<PublicKey, Error> {
        use util::{TAG_PUBKEY_EVEN, TAG_PUBKEY_ODD};

        if !(p[0] == TAG_PUBKEY_EVEN || p[0] == TAG_PUBKEY_ODD) {
            return Err(Error::InvalidPublicKey);
        }
        let mut x = Field::default();
        //反序列化处理x坐标
        if !x.set_b32(array_ref!(p, 1, 32)) {
            return Err(Error::InvalidPublicKey);
        }
        //将处理结束后的坐标转换为Affine类型的变量
        let mut elem = Affine::default();
        elem.set_xo_var(&x, p[0] == TAG_PUBKEY_ODD);
        if elem.is_infinity() {
            return Err(Error::InvalidPublicKey);
        }
        if elem.is_valid_var() {
            Ok(PublicKey(elem))
        } else {
            Err(Error::InvalidPublicKey)
        }
    }

在传入set_xo_var时就设置p[0] == TAG_PUBKEY_ODD,即公钥y坐标为奇数。
在set_xo_var函数中实现设置一个仿射坐标表示的群元素,这个群元素的x坐标与给定x坐标一致。

在set_xo_var函数中调用函数set_xquad,实现群元素坐标x和y的设置,设置之外还是为了验证给定的x坐标是不是有效,即将x代入到椭圆曲线方程中能否找到一个对应的y点。

    pub fn set_xo_var(&mut self, x: &Field, odd: bool) -> bool {
        if !self.set_xquad(x) {
            return false;
        }
        //将在set_xquad中得到的y坐标正常化
        self.y.normalize_var();
        //判断在set_xquad中得到的y坐标是否为奇数,
        //如果不是奇数使用neg函数得到对应奇数坐标
        if self.y.is_odd() != odd {
            self.y = self.y.neg(1);
        }
        true
    }

函数set_xquad实现设置一个仿射坐标表示的群元素,这个群元素的x坐标等于给定x坐标,并求出这个x坐标对应椭圆曲线方程的纵坐标y。如果存在一个给定x坐标的椭圆曲线群下的坐标,则返回值为真。

    pub fn set_xquad(&mut self, x: &Field) -> bool {
        self.x = *x;
        let x2 = x.sqr();
        let x3 = *x * x2;//求x的三次方
        self.infinity = false;
        let mut c = Field::default();
        c.set_int(CURVE_B);
        c += x3;//求 x的三次方 加 b
        let (v, ret) = c.sqrt();//求平方剩余,即求一个数v,v的平方模p结果为c
        self.y = v;//求得的v即为纵坐标y
        ret
    }

序列化

本代码实现将未压缩公钥序列化。具体的序列化实现通过调用fill_b32函数实现,对x和y分别使用fill_b32函数,将坐标序列化。

pub fn serialize(&self) -> [u8; util::FULL_PUBLIC_KEY_SIZE] {
        use util::TAG_PUBKEY_FULL;

        debug_assert!(!self.0.is_infinity());

        let mut ret = [0u8; 65];
        let mut elem = self.0;

        elem.x.normalize_var();
        elem.y.normalize_var();
        elem.x.fill_b32(array_mut_ref!(ret, 1, 32));
        elem.y.fill_b32(array_mut_ref!(ret, 33, 32));
        ret[0] = TAG_PUBKEY_FULL;

        ret
    }

fill_b32实现过程如下所示,Field元素的公钥一个x或y坐标包括10个u32数组元素,序列化后包括32个字节,用32个u8数组元素接收,实现序列化过程。

pub fn fill_b32(&self, r: &mut [u8; 32]) {
        debug_assert!(self.normalized);
        debug_assert!(self.verify());

        r[0] = ((self.n[9] >> 14) & 0xff) as u8;
        r[1] = ((self.n[9] >> 6) & 0xff) as u8;
        r[2] = (((self.n[9] & 0x3f) << 2) | ((self.n[8] >> 24) & 0x3)) as u8;
        r[3] = ((self.n[8] >> 16) & 0xff) as u8;
        r[4] = ((self.n[8] >> 8) & 0xff) as u8;
        r[5] = (self.n[8] & 0xff) as u8;
        r[6] = ((self.n[7] >> 18) & 0xff) as u8;
        r[7] = ((self.n[7] >> 10) & 0xff) as u8;
        r[8] = ((self.n[7] >> 2) & 0xff) as u8;
        r[9] = (((self.n[7] & 0x3) << 6) | ((self.n[6] >> 20) & 0x3f)) as u8;
        r[10] = ((self.n[6] >> 12) & 0xff) as u8;
        r[11] = ((self.n[6] >> 4) & 0xff) as u8;
        r[12] = (((self.n[6] & 0xf) << 4) | ((self.n[5] >> 22) & 0xf)) as u8;
        r[13] = ((self.n[5] >> 14) & 0xff) as u8;
        r[14] = ((self.n[5] >> 6) & 0xff) as u8;
        r[15] = (((self.n[5] & 0x3f) << 2) | ((self.n[4] >> 24) & 0x3)) as u8;
        r[16] = ((self.n[4] >> 16) & 0xff) as u8;
        r[17] = ((self.n[4] >> 8) & 0xff) as u8;
        r[18] = (self.n[4] & 0xff) as u8;
        r[19] = ((self.n[3] >> 18) & 0xff) as u8;
        r[20] = ((self.n[3] >> 10) & 0xff) as u8;
        r[21] = ((self.n[3] >> 2) & 0xff) as u8;
        r[22] = (((self.n[3] & 0x3) << 6) | ((self.n[2] >> 20) & 0x3f)) as u8;
        r[23] = ((self.n[2] >> 12) & 0xff) as u8;
        r[24] = ((self.n[2] >> 4) & 0xff) as u8;
        r[25] = (((self.n[2] & 0xf) << 4) | ((self.n[1] >> 22) & 0xf)) as u8;
        r[26] = ((self.n[1] >> 14) & 0xff) as u8;
        r[27] = ((self.n[1] >> 6) & 0xff) as u8;
        r[28] = (((self.n[1] & 0x3f) << 2) | ((self.n[0] >> 24) & 0x3)) as u8;
        r[29] = ((self.n[0] >> 16) & 0xff) as u8;
        r[30] = ((self.n[0] >> 8) & 0xff) as u8;
        r[31] = (self.n[0] & 0xff) as u8;
    }

压缩公钥序列化实现同理,由于是压缩公钥只需序列化x坐标即可,重新调用上面的fill_b32函数实现序列化过程。最后对y坐标的奇偶性判断,加上对应的压缩前缀。

pub fn serialize_compressed(&self) -> [u8; util::COMPRESSED_PUBLIC_KEY_SIZE] {
        use util::{TAG_PUBKEY_EVEN, TAG_PUBKEY_ODD};

        debug_assert!(!self.0.is_infinity());

        let mut ret = [0u8; 33];
        let mut elem = self.0;

        elem.x.normalize_var();
        elem.y.normalize_var();
        elem.x.fill_b32(array_mut_ref!(ret, 1, 32));
        ret[0] = if elem.y.is_odd() {
            TAG_PUBKEY_ODD
        } else {
            TAG_PUBKEY_EVEN
        };

        ret
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要通过secp256k1签名获取公钥,您可以使用Java的Bouncy Castle库来实现。以下是一个示例代码片段,可以帮助您开始: ```java import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.crypto.signers.HMacDSAKCalculator; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.SignatureException; public class ECDSASignerExample { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA", "BC"); keyGen.initialize(256); KeyPair keyPair = keyGen.generateKeyPair(); ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(((java.security.interfaces.ECPrivateKey)keyPair.getPrivate()).getS(), ECUtil.getNamedCurveByName("secp256k1")); ECPublicKeyParameters publicKey = new ECPublicKeyParameters(((java.security.interfaces.ECPublicKey)keyPair.getPublic()).getQ(), ECUtil.getNamedCurveByName("secp256k1")); String message = "Hello, world!"; byte[] messageHash = sha256(message.getBytes("UTF-8")); ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); signer.init(true, privateKey); BigInteger[] signature = signer.generateSignature(messageHash); ECPoint q = ECUtil.getCurve("secp256k1").decodePoint(publicKey.getQ().getEncoded(false)); boolean verified = signer.verifySignature(messageHash, signature[0], signature[1], q); System.out.println("Verified: " + verified); } private static byte[] sha256(byte[] input) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); return digest.digest(input); } } ``` 这个示例代码片段生成一个新的密钥对,使用SHA-256哈希算法对消息进行哈希,在哈希消息上对其进行 ECDSA 签名,并验证签名。您可以根据需要修改代码来适应您的实际用例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值