Ring Signatures in the CLSAG era

Let's understand what are CLSAG (Concise Spontaneous Anonymous Group) signatures, how it differs from MLSAG and why the Monero developers implemented it on 17 October 2020. A simple description in layman's terms and a comprehensive explanation of the verification code is provided here.

CLSAG in Layman's terms

The purpose of implementing CLSAG was to reduce the operational cost (storage and computation) by removing the decoys in the commitment rings, so they won't be stored in the blockchain anymore and therefore a lot of space will spared. This also promotes the linkability between the commitment and the public key ring. CLSAGs are half way between bLSAGs and MLSAGs. Instead of having only one dimension like bLSAG (only for the public key members) or two dimensions like MLSAG (one for the public key and another for the commitment members), CLSAG will only store the signatures for the public key members and only the commitment that unlocks the real output (which nobody can tell which one is the real one by just looking at the commitment or signatures).

Fig. 1 - CLSAGs (one ring linked to one commitment).

CLSAG Signatures in practice

Before understanding the generation and verification function of a CLSAG signature (generate_CLSAG(msg,p,P,z,C_offset,C,C_nonzero) and check_CLSAG(msg, s, c1, D, P, C_nonzero, C_offset)), we need to understand its inputs arguments and to what they relate to.

Message (m)

The message represents all the information of a transaction (version, unlock time, inputs, outputs, extra, rct_signatures and rct_prunable) except the ring signatures (CLSAGs). It can only be signed by the person owning the private key corresponding to the published public key.

In the CLSAG era, the message is divided into three parts:

The final message $m$ is the hash of the sum of the hashes of each part, i.e. $m =\mathcal{H} (\mathcal{H}(P1) + \mathcal{H}(P2) +\mathcal{H}(P3))$

Let's get the transaction c39652b79beb888464525fee06c3d078463af5b76d493785f8903cae93405603 as example.

Here we have:

P1 = 02000102000be9aac314d8e710844d8d258133d9f701b0649e568b0cb50b1dea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de380200025155f659da61b507b0b8591cbef0ba1534b9db29a69be4a933f7897e58870b250002002de4643160fb8351a841f8079aa5af2ac62c9631bb2d5a48fcdb564cb699d12c01eaaa5acba0bc44657da783903d3de7febf7124ae03cf578938244068b475d906020901da0190b93466979e

P2 = 0580efcf037eafd43a457b940e592f261bd165059a9d3b92aa3baaed18346157d1361e30c553df17940985b375fc89b9c09d613d5daf007ed1ce05128cfc4d37181a5f2d6f56690357032dbcc6e85424e5ad793388

P3 = df166e5b93e427f8b86079e0de0695d24a4ac5098d98876da5f892359c982015fc0461847fd2955f6099cfc37d348bdd003f064a45c6959478807f7cb6263fc539b2616e13117fc82303aba9afeed67104c344f971a182e741436bcc040468fa6307bb478ed8ef47397e603fe7d50d3c56f5c50c4410db470e21c04b5b883a94c78bc19587d8a611c701d51b956ae06e92987ecdcd5237a65725fdcf7b52a9085831c7214b1282fbd1faf81277a6519c941907f0ef838576e610272512d1860aa19ac9aec33168b06c937d1ead2e63e58345252be0a5fe15d38a371a6347355d16104b19f1d4a4c18e07dba72bbf8c8150bade347831b446f2ad9f5af4aa91cd35ddf3c64b3f0690c77634efd4ef07b817c6bb90a29f917769cd2978d37a14ed61b437858c93100f55b82ac7f94c0f83ac68a54df1e4d2c54b9c96661f7a41ed9fff8b28fc31949a07a7b298b5819ac7fe7c9490b20a1107ba6f6b94e6d1a58fab09bed6d096edaaea887addea1b7735228bdff409674c0f8bfceb815cdbae6fc5de41379ff842eebe205611c58d80371feb97b1f2a23fb65a474665f9911134da1c0a6f6d1fa7b6ad659f45354178270f5541eb948cbc861c53705982fa67f88c70cb936e86bf45bb17d6214ab716dc0c5fa3b7f3a181e23fad244741c1b6c44c557cf0e4dfe7a1acf75d39bfb810a798b33bcbfc73d9a11b0c648c84ed6e28b27e4aa8681aa33c0779e01ac2d16f58031d92fae578d92b6ab5d7c98a47b64ead0ad4f036922b4604d1b23dd4f4b798bae917294fc458000e7ce1449060a98c62bb1d86a0841f26e70550778103fcc3d70b9adcdb32cc750016b88178513e94d61eb65c8381532feb3368f3db60638f3b88575dded95eb92910d16c51351e7379e5df4f1fd2ffe1b80b958ef6f189ff0483a802bd1c49ed56131e1cc02dbe0949251adbc980b5a7f45317bab8be87961b6b9496d24779023f77dd1b4552fc0b522e857f38e9c63349955e3be4b3a15e86aa96943900797b8139cbfdcd940903

hash(P1) = ffc97c3febc55e14f0c2ee78092e023935121e7d750cbeb65511e4368242fe22

hash(P2) = 5778b38f679c0bfca78e8ab9a1ba35f029e0e1b33a3be91849cbf30c18c7a6f1

hash(P3) = 93525ff81e8f015f06bf9668c8ef67844de4deac85de2edea4aa0e7298d1da2b

message = hash(ffc97c3febc55e14f0c2ee78092e023935121e7d750cbeb65511e4368242fe225778b38f679c0bfca78e8ab9a1ba35f029e0e1b33a3be91849cbf30c18c7a6f193525ff81e8f015f06bf9668c8ef67844de4deac85de2edea4aa0e7298d1da2b) = 686cc5232f8d0d90c6a447b10b5296c98b0b4ad5e2f88f278a6bd8f3eeb13dbf

Pubs, Masks and C_offset

Let's build our Public Key vectors, which means, let's get the participants (members) of our ring as represented in figure 1.

The members of the ring can be retrieved by looking at the field ['vin']['key_offsets'] stored in a transaction. The members are previous transactions outputs (stealth addresses and commitments) that contains funds to be spent from.

It is important to notice that every output (stealth address and commitment) is stored by its index number (which is the index position where that output can be found in the pile of its respective amount). This index number is what the key_offset represents. Alongside with that, we can also retrieve the block_height and tx_id.

In the case of a RCTTypeCLSAG transaction, we also have one pseudo output for each input (or key_image). Which can be retrieved from a transaction at the 'pseudoOuts' field.

Finally, from the key_offsets, let's retrieve what we need to build our Public Key vector which has the dimensions $n$, where $n$ is the number of participants in one ring which is the length of the key_offsets vector. In order to do so, we need to extract the 'key' which represents the stealth address of a previous transaction.

We also have to extract the 'mask' which represents the commitment to a value of a previous transaction.

Therefore, our algorithm to build the Public Key vector, with $n$ being the length of the key_offsets, looks like:

RCTTypeCLSAG(Type 5)

For $i = 0 .. n$:

$pubs[i] = \text{get_outs(key_offsets[}i\text{])["key"]}$

$masks[i] = \text{get_outs(key_offsets[}i\text{])["mask"]}$

Notice that we are only verifying one input or key_image at a time. We need to repeat this procedure for as many inputs that a transaction has.

See an example below.

Let's get the transaction c39652b79beb888464525fee06c3d078463af5b76d493785f8903cae93405603 as example.

It is a RCTTypeCLSAG and we have one input only.

pubs, masks, C_offset:

Stealth addresses ring members: 
pubs = [09a9cd2976908e172096b41bd6e701baf89a54580ddaf7338e837f944cacdf4e, d899b26b27778a5bb4c05c537a721f7822442ee4c6d9feebac62a15d76e23efe, a65d0610b529f918d486abe7bdcc5a4aaea3e40ae1791cda91399113d9727136, 08f4824943cc66da49aa4f2d4e7b6370026d1bc3d3e542c796a750f0d9a53864, 3f6e0c940ee6d00f23c3bfffd7a49cd771da7b3814f8023c87e070d5a94b312f, 5e0709be62408e42d4f0448ce928514cc2f2e2fa7be8ef1d71fca8493d9392cb, 5073c115a4b9b726c5131c83a84c239cb38a408e404f8669ceb68b9aefc4ade9, 8ca62cc1330761cdb7f7f0d7d6028e3ffd94c94a55f703adc62a024f650f88c7, c81f7662f255c82d563db4635f8ecf50e7b3378c739d684a0d43b808f9cca905, f3d71b7bdb06602fbeafec7bc99b87e846eff10faf8c4119a8f2e39ac8754a9f, 41bd8dab5bdc5af694dde135a7278e8e0aab0cb863483e037a73fd17a6a8a36a]

Masks ring members: 
masks = [c2bd552d46323f384822d9544cd06ad50183ca914b4b07f588656359e97a6505, 16009aac699f44994af5373bab8cb4fef8488600bb4c277d1edccd4aa564060c, 32538a3f879172c9d0ba110c770518ba1693dde7eb37359fceeb1f17940d3963, 598f876a153703081c62b6e64f81ea20246fe8b453b9bd90642167d1b44a02d0, d53432cd1f3e7a9403c802ea8adb64db0431da00cd3876c73415404a28addafc, 4df8398cced781735df7a609285dd87901d90ed5f0609a2f498f42f8ea30e3f1, b4454b4a67b099bcb4353a53f9a37b4057314410536b5ca514f9a669dec7020f, 625b654b5220ed78ca183cd2214bce68e2548ed1c61a23e33234fe7f2ca76bfb, 251e9855b7168b18539ea4da1e1e7ca1edb8cdb5edadcd2852887a1cf48dfd79, 32146e6d718c7349c261de39846aa9aa835b68cc25fa36c3beafde83cd7aadcd, 61d19ce3bbbd02bc242dafc241649d5dd82060d8314e6b9f837e3a72f2007c5c]


PseudoOuts: 
C_offset = c4cf22334e32fd33193fbfa1c4cb9e3182fb2357cf7a800f552b87579cc41d99

Key Image (KI)

The key image is just the secret key times the hash of the public key. It can be defined by the equation: $KI = x\mathcal{H_p}(P)$. Where $I$ is the key image, $x$ is the secret key and $\mathcal{H_p}(P)$ is the hash mapped to a point of the public key generated by $x$.

Notice that this 'secret key' in the context of Monero is actually the secret key to unlock an output (stealth address) and it is a function that depends on the transaction public key, the secret view key, the secret spend key and the output index. Therefore, one person can create multiple key_images with the same secret view and spend keys, if this person have multiple outputs addressed to him/her, of course. The equation below shows what this secret key represents but please check how 'One-time addresses' are created, if you want to further investigate it. You will find a good resource here.

\[ k_t^O = \mathcal{H_s}(rK_t^v,t) + k_t^s \qquad \text{---> secret key} \\ \] \[ K_t^O = k_t^O G \qquad \text{---> public key}\\ \]

Let's suppose that we have the following private (secret) key: x = 09321db315661e54fe0d606faffc2437506d6594db804cddd5b5ce27970f2e09

From it, we can derive the public key by the definition of a point in an elliptic curve.

The public key (P) is defined by: P = xG

Using the Monero elliptic curve definitions (Edwards25519), we have P = cd48cd05ee40c3d42dfd9d39e812cbe7021141d1357eb4316f25ced372a9d695

In order to make it possible to perform another point multiplication, we need to treat P as a hash and map it to a point in the elliptic curve. The $\mathcal{H_p}$ is responsible for that. In our case, $\mathcal{H_p}(P)$ is the Point $\mathcal{H_p}(P)$ = c530057dc18b4a216cc15ab76e53720865058b76791ff8c9cef3303d73ae5628

Finally, we need to perform another point multiplication, which is $KI = x\mathcal{H_p}(P)$. The key image in our case is $KI$ = d9a248bf031a2157a5a63991c00848a5879e42b7388458b4716c836bb96d96c0

In the context of CLSAG, let's get the key_image of the following transaction: c39652b79beb888464525fee06c3d078463af5b76d493785f8903cae93405603

Key Image: ea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de38

Generating a CLSAG signature

The idea behind the CLSAG signatures is also simple. We want to obfuscate the sender by proving that someone in the ring set signed the message and transferred the funds without being able to specify exactly who.

In order to generate a CLSAG signature, let's see what we need generate_CLSAG(msg,p,P,z,C_offset,C,C_nonzero) to pass as parameters:

Before we talk about the CLSAG scheme, we need to define some variables. Let's get started.

If $l$ is the secret index of the ring member signing the transaction, then we have: $P[l]=pG$ , $C[l]=zG$ and for all i, we have $C_{nonzero}[i]=C[i]+C_{offset}$ (for hashing purposes).

Let's start by defining some domain names which will be hashed with some values later:

Now let's define some other variables that will be needed later:

Finally, let's get the variables $mu_P$ and $mu_C$ which are separated by the domain name:

Now we are ready to see how the ring signature generation scheme works in CLSAG.

Considering $\alpha$ a random scalar, let's start defining the following points:

Now define $c$ as:

\[ c =\mathcal{H_s}\text{(str_round+strP+strC_nonzero+C_offset+msg+aG+aH)} \]

We want $c1$ (a new variable) to be equal to $c$ when the index i=(l+1)%n is equal to zero. Therefore, if $i == 0$ then $c1 = c$.

Now let's start our loop at i and do it n times, where is the number of ring members, i,e: for i = l+1,l+2,..,n-1,0,1,..,l-1,l.

\[ s[i] = \text{random_scalar()} \\ \] \[ cp = c \, mu_P \\ \] \[ cc = c \, mu_C \\ \] \[ L = s[i]G + cpP[i]+ ccC[i] \\ \] \[ R = s[i]\mathcal{H_p}(P[i]) + cpI + 8ccD \\ \] \[ \text{str_hash = str_round+strP+strC_nonzero+str(C_offset)+str(msg)} \\ \] \[ \text{str_hash += str(L) + str(R)} \\ \] \[ c = \mathcal{H_s}\text{(str_hash)} \\ \]

We increase the counter and check if it is equal to zero:

\[ i = (i+1) \% n \\ \] \[ \text{ if i == 0 : { $c1 = c$ }} \\ \]

Finally, we need to modify the signature s at the index l.

\[ s[l] = alpha - c(p \, mu_P+ z \, mu_C) \]

As you noticed, the signature is basically composed by the random numbers that we generated except at the index where we control the funds of one of the members. But of course, for an external reader, he cannot tell where the secret index is.

The final signature will be s,c1 and D

\[ \sigma = \{ s, c1, D \} \]

Let's suppose that we have the following parameters as inputs:

msg = str('612ebe1b1bfdd4f49732afedc43f4c815f899e190f316eda75fad5423e53b8e6')
p = Scalar('073d1d6d6d3ec1b47b03b40d4fd9fec97fca4b61ed49a66bb32e818b5d9c710f')
z = Scalar('64aca5a5c70ff73ef3a192f8286efb1353eb8c642ef96d8c263d8b4ea2259b0c')
C_offset = Point('feae5544786270b394a8f34f753d550be92a318c291926b7d9bfa7fb4cde3745')
l = 1

P = PointVector([ Point('af837eefa0de5135777b7cc52f8a68d8a883b22f156bf0e04873548b9caa8edb'),Point('1e9e346d687fc94c102a0ee70d050ba70d8135a366581a98e2c45e8926e8ed83'),Point('267b7f48f760219278443f40d5edb097d82b1aa7e7e6a6cae084f19d18f7b3c7'),Point('88afa48a95ce58cdc9eb988abdc9c3b9226efe1ddd12b6ab0a4afec3be6faee7'),Point('9be7a0ea0cee158f788f0ab788e0ec3db67f2a288a8a64c0e1d85056d06ba908'),Point('82f204a77a869456afc5a345a1770dc1beed379b2bfb3c331294220d6d9ca0c6'),Point('99b32f89d6b860ad58109829b1eed052188a1d6a8ba1e965ae4b82fae7f02411'),Point('abddfd5f34fa96cfc93058feb189e6433cbdcdbfd9e13286d9109448c142e720'),Point('7f23a1a5680516f6f3056ffff709e1780d1e5fa826ebfd097b6f0fd98e758cc3'),Point('5ad9278b0be168f15b9fa5c68537c68f3f37f03d7999a1e6e8f4e28882dc4320'),Point('993f2ef0adc11ddd1f7414f5049cf94cb3742423b5309bc9944e0752fcdcf342'),])
 
C = PointVector([ Point('de50d6aabc7bd68eff67bded347d494140d8ed0c12a6785a0ba3dad497b24c86'),Point('a1ec7c218ae64b7e0dad3330ab1e1abd7902aca944992b4ba8caa9201682fa07'),Point('6ca7bd3e3863ea0a8710fe893b8a27f0f4c834d7a82699db1711eaacab079f24'),Point('2f07ff1641083850605c04059e17aad0241e10b65994f1ffcc34f3e2c80e716f'),Point('d4f2e9080afe0296164554e5846cea8955b489af4f8bb531a8e34a1f80e7a02b'),Point('1172a23df246f5973b12541d2068afa16159a1fdc421aab27fca6b9fab09bfab'),Point('88f1b21020e38d50c7fd98feefc6253291613db00129654d786320318a6880d4'),Point('8522ddaae47f142fa34654d828674b51357e74c75cbe55501b600806c5506576'),Point('faab64f49cac9dcf53cb0de21a41be20a3234369b1b5609305fa737f4a9413ce'),Point('e751f0ef44cdeb6d5e5821e8013e7de02e9522651b8b9763a304468979b86008'),Point('d6f3ab5d6f9bf2569c12f7fa800dbfe66a69e406db7e6318cfb26ab1e966d262'),])
 
C_nonzero = PointVector([ Point('ef056230343b70feb26fc4c3cde31c96ec6481f9815893240c72cf2e55a20678'),Point('0fcba4f8a5794d45c0bb0c9bae361857285a05a72704f0af27319dfbb02e5868'),Point('0b77f1a2395294b40cf5a3b1d7e1d9e3615e3262ca2e2cd5e8e4c975d01d4ff4'),Point('94fe96babf8986539db8ab69a219733c5280d5d2b5d7d64af70f04fb66eb402e'),Point('b308f21f6749df01857bd39829f1c84beed823a569577a78f447f1cbdd7f9852'),Point('b8178cc7aff509d42872eec29e0f089c2128f9a2e7fd3a0494e20cad6216cf47'),Point('d148d43672a326e22a2374d9c865cabfbeb555d7cb7e5eb9899c9c4fecb05936'),Point('cd478010293e2f298f3b21243e14fdb4f1806ae030b3c3a65585e60ad2c61a56'),Point('166d0ebf687ccb8a459c407261be109304eeef9f89e8cb6bd40b5f8515f9c128'),Point('eb90be4a8d3810d05113e2eec14bb7e0c3d244a974a1732e19c81716042a7984'),Point('43f3aaf9d2f142621661985916cd8d7e7e9c1c9eeaefdbd3fab582520c471c77'),])



Let's also suppose that we execute the following code to produce the signatures $s$ and $c1$ and $D$:

def generate_CLSAG(msg,p,P,z,C_offset,C,C_nonzero,Seed=None):
    inv8 = Scalar(8).invert()
    n = len(P) # ring size

    # Recover the private key index
    l = None
    for i in range(n):
        if P[i] == dumber25519.G*p and C[i] == dumber25519.G*z:
            l = i
            break
    if l is None:
        raise IndexError('Private keys must correspond to public keys!')

    # Construct key images
    I = hash_to_point(str(P[l]))*p
    D = hash_to_point(str(P[l]))*z*inv8

    domain0 = 'CLSAG_agg_0' 
    domain1 = 'CLSAG_agg_1' 
    domain_round = 'CLSAG_round'

    str0 = str(Scalar(0))
    str_agg0_aux = domain0.encode("utf-8").hex()
    str_aux = str0[len(str_agg0_aux):]
    str_agg0 = str_agg0_aux + str_aux

    str_agg1_aux = domain1.encode("utf-8").hex()
    str_aux = str0[len(str_agg1_aux):]
    str_agg1 = str_agg1_aux + str_aux

    str_round_aux = domain_round.encode("utf-8").hex()
    str_aux = str0[len(str_round_aux):]
    str_round = str_round_aux + str_aux

    strP = ''
    for i in range(len(P)):
        strP += str(P[i])

    strC = ''
    for i in range(len(C)):
        strC += str(C[i])

    strC_nonzero = ''
    for i in range(len(C_nonzero)):
        strC_nonzero += str(C_nonzero[i])

    # Now generate the signature
    mu_P = hash_to_scalar(str_agg0+strP+strC_nonzero+str(I)+str(D)+str(C_offset))
    mu_C = hash_to_scalar(str_agg1+strP+strC_nonzero+str(I)+str(D)+str(C_offset))
    s = [None]*n

    alpha = random_scalar()

    # Private index
    aG = dumber25519.G*alpha
    aH = hash_to_point(str(P[l]))*alpha
    c = hash_to_scalar(str_round+strP+strC_nonzero+str(C_offset)+str(msg)+str(aG)+str(aH))

    i = (l+1) % n
    if (i==0):
        c1 = copy.copy(c)

    while (i!=l):
        s[i] = random_scalar()
        cp = c*mu_P
        cc = c*mu_C

        L = s[i]*dumber25519.G + cp*P[i]+ cc*C[i]

        R = s[i]*hash_to_point(str(P[i])) + cp*I + cc*D*Scalar(8)

        str_hash = str_round+strP+strC_nonzero+str(C_offset)+str(msg)
        str_hash += str(L) + str(R)

        c = hash_to_scalar(str_hash)

        i = (i+1) % n
        if i==0:
            c1 = copy.copy(c)

    s[l] = alpha - c*(p*mu_P+mu_C*z)

    return s,c1,D

In the end, a valid signature would look like:

s:
[6f3b787bdbebe40bea262d00d9b769d01637d9f787745715b180a38294293009, b8ee8308036faf269b8a2adeff79cdcf6c066cddd794044f2c87cd89d9172d0a, d61f57e4fb21dca51f7d7ed30438b2882ffece123216dc8e5b22d13110afc00b, 074a48246b63d07870db2d1d6393696d2610883459949e66e234ed6337257f00, cd4c787d6fc26f58d9add0d728dbb6f3481c4f2b065caf2e51c5c7e92f754807, 0db66271944ce00ba6835ce2e8bed77803d5a7fc2a15ab7d13f33d37e7738e05, 706555307d6f5e76598c6d84d76528b510e66ad47aa7057eb661f05348479004, e0bac7669115afbc13c3ce5a23302c6f7093bfd8dd39461d27d5ca96cb914309, 0ed2a3bea98b0f7a5b46e4253cd46670907d2eb8c9cecf361363aced79fcc000, 166d95debf8ad7ce781543736c65ed9f0358c97573c8930a6e78d396a34b500e, 2c979f78029662bcbc58f71c06cc3ba88d79c7a68bd683da18e529c6a678c104]

c1:
437a25418bd78a4712740c36c84f7b24a983d91e064456a3502bc3da4531ec0f

D:
c73294efab521c03b43a97ddd9313decb40aed61fa12395772a18751d319cc3e

Checking a CLSAG signature

To check if the signature is valid let's first understand the math behind it. The verification function looks like check_CLSAG(msg, s, c1, D, P, C_nonzero, C_offset), where:

In the same way we did to generate a CLSAG ring signature, let's start by defining some domain names which will be hashed with some values later:

Now let's define some other variables that will be needed later:

Finally, let's get the variables $mu_P$ and $mu_C$ which are separated by the domain name:

Now we are ready to see how we can check the ring signature scheme in CLSAG.

Let's copy c1 to c and execute the following steps for i = 0 .. n-1:

\[ cp = c \, mu_P \] \[ cc = c \, mu_C \] \[ L = s[i]G + cpP[i]+ cc\text{(C_nonzero[i] - C_offset)} \] \[ R = s[i]\mathcal{H_p}(P[i]) + cpI + 8ccD \] \[ \text{str_hash = str_round+strP+strC_nonzero+str(C_offset)+msg} \] \[ \text{str_hash += str(L) + str(R)} \] \[ c = \mathcal{H_s}\text{(str_hash)} \]

Lastly, if $c$ equals $c1$, then the signature is valid.

Let's check the signature from the examples above: c39652b79beb888464525fee06c3d078463af5b76d493785f8903cae93405603.

We have as inputs:

message: 
686cc5232f8d0d90c6a447b10b5296c98b0b4ad5e2f88f278a6bd8f3eeb13dbf

pubs: 
[09a9cd2976908e172096b41bd6e701baf89a54580ddaf7338e837f944cacdf4e, d899b26b27778a5bb4c05c537a721f7822442ee4c6d9feebac62a15d76e23efe, a65d0610b529f918d486abe7bdcc5a4aaea3e40ae1791cda91399113d9727136, 08f4824943cc66da49aa4f2d4e7b6370026d1bc3d3e542c796a750f0d9a53864, 3f6e0c940ee6d00f23c3bfffd7a49cd771da7b3814f8023c87e070d5a94b312f, 5e0709be62408e42d4f0448ce928514cc2f2e2fa7be8ef1d71fca8493d9392cb, 5073c115a4b9b726c5131c83a84c239cb38a408e404f8669ceb68b9aefc4ade9, 8ca62cc1330761cdb7f7f0d7d6028e3ffd94c94a55f703adc62a024f650f88c7, c81f7662f255c82d563db4635f8ecf50e7b3378c739d684a0d43b808f9cca905, f3d71b7bdb06602fbeafec7bc99b87e846eff10faf8c4119a8f2e39ac8754a9f, 41bd8dab5bdc5af694dde135a7278e8e0aab0cb863483e037a73fd17a6a8a36a]

masks:
[c2bd552d46323f384822d9544cd06ad50183ca914b4b07f588656359e97a6505, 16009aac699f44994af5373bab8cb4fef8488600bb4c277d1edccd4aa564060c, 32538a3f879172c9d0ba110c770518ba1693dde7eb37359fceeb1f17940d3963, 598f876a153703081c62b6e64f81ea20246fe8b453b9bd90642167d1b44a02d0, d53432cd1f3e7a9403c802ea8adb64db0431da00cd3876c73415404a28addafc, 4df8398cced781735df7a609285dd87901d90ed5f0609a2f498f42f8ea30e3f1, b4454b4a67b099bcb4353a53f9a37b4057314410536b5ca514f9a669dec7020f, 625b654b5220ed78ca183cd2214bce68e2548ed1c61a23e33234fe7f2ca76bfb, 251e9855b7168b18539ea4da1e1e7ca1edb8cdb5edadcd2852887a1cf48dfd79, 32146e6d718c7349c261de39846aa9aa835b68cc25fa36c3beafde83cd7aadcd, 61d19ce3bbbd02bc242dafc241649d5dd82060d8314e6b9f837e3a72f2007c5c]

C_offset:
c4cf22334e32fd33193fbfa1c4cb9e3182fb2357cf7a800f552b87579cc41d99

I:
ea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de38

s:
[ce41249b9e7e4a933b88a204b1ded0e2ba0fa6a1cf3ba4c3ffcc0ca96b06b400, 051d66ede7013ccc453e4f042e17e1091a536456460fe3551f36c99c05d59105, 802ee93d645167ee586e71fcc23c33d2362754065955eca45f294bb28f389608, 26166dfdde409f76f75366234b0a08f6e49caa1c2169e8d9376673e9835dc709, bb94e4d8a164ee6278998e891e05086ad8996e496e833d975cb3d4fceb3f7203, a7b365891227448b2e968b5bb653b29741bad9ba107b417354e6e3e5acf5710a, 346e232e9f02606a31bd8b692a67578637a5ff82339dde31425ad59ada173601, 84f24e6b542810ec43ef11d6a8b2eba53b288ca445d1e1a0daf059320978160e, b610631a9b382291418309fc3ceecf3a08900ff5d301044a3cf565567129b10d, c2aa970f43493920c4ef3df1c408e12bbcfb01573b79e296e34912cde6271603, 3f73cf74f42b0fde655f2848bec39fd63f3485499147fe170861e9e0eae0e90d]

c1:
400328b595456b6451dc07eac7c8f6849dc065bb7f5ac49cff1530658bcc4d09

D:
d03e2f9611a5561dc3a90b3b2f3d933dc110db73904aeee5abcd2362a0e1b045

If we execute the following code, we will see that the signatures match.

def check_CLSAG(msg, s, c1, D_aux,I, P, C_nonzero, C_offset):
    
    domain0 = 'CLSAG_agg_0' 
    domain1 = 'CLSAG_agg_1' 
    domain_round = 'CLSAG_round'

    str0 = str(Scalar(0))
    str_agg0_aux = domain0.encode("utf-8").hex()
    str_aux = str0[len(str_agg0_aux):]
    str_agg0 = str_agg0_aux + str_aux

    str_agg1_aux = domain1.encode("utf-8").hex()
    str_aux = str0[len(str_agg1_aux):]
    str_agg1 = str_agg1_aux + str_aux

    str_round_aux = domain_round.encode("utf-8").hex()
    str_aux = str0[len(str_round_aux):]
    str_round = str_round_aux + str_aux

    D = copy.copy(D_aux)

    strP = ''
    for i in range(len(P)):
        strP += str(P[i])

    strC_nonzero = ''
    for i in range(len(C_nonzero)):
        strC_nonzero+= str(C_nonzero[i])

    mu_P = hash_to_scalar(str_agg0+strP+strC_nonzero+str(I)+str(D)+str(C_offset))
    mu_C = hash_to_scalar(str_agg1+strP+strC_nonzero+str(I)+str(D)+str(C_offset))

    c = copy.copy(c1)

    print('c: ')
    print(c)

    i = 0
    n = len(P)

    while (i < n):
        cp = c*mu_P
        cc = c*mu_C

        L = s[i]*dumber25519.G + cp*P[i]+ cc*(C_nonzero[i] - C_offset )
        R = s[i]*hash_to_point(str(P[i])) + cp*I + cc*D*Scalar(8)

        str_hash = str_round+strP+strC_nonzero+str(C_offset)+msg
        str_hash += str(L) + str(R)

        c = hash_to_scalar(str_hash)
        i = i+1

        print('c: ')
        print(c)

    c_final = c - c1

    import ipdb;ipdb.set_trace()

    return c_final