பிரதான உள்ளடக்கத்திற்குச் செல்

ஆஃப்லைன் தரவு ஒருமைப்பாட்டிற்கான மெர்க்கல் சான்றுகள்

சேமிப்பு
மேம்பட்டவை
Ori Pomerantz
30 டிசம்பர், 2021
8 நிமிட வாசிப்பு

அறிமுகம்

ஆயிரக்கணக்கான கணினிகளில் சேமித்து வைக்கப்பட்டுள்ள Ethereum சேமிப்பகத்தில் எல்லாவற்றையும் சேமிக்க நாங்கள் விரும்புகிறோம், இது அதிக கிடைக்கும் தன்மை (தரவை தணிக்கை செய்ய முடியாது) மற்றும் ஒருமைப்பாடு (தரவை அங்கீகரிக்கப்படாத முறையில் மாற்ற முடியாது) ஆகியவற்றைக் கொண்டுள்ளது, ஆனால் 32-பைட் வார்த்தையைச் சேமிப்பதற்கு பொதுவாக 20,000 எரிவாயு செலவாகும். நான் இதை எழுதும் நேரத்தில், அந்தச் செலவு $6.60-க்குச் சமம். ஒரு பைட்டுக்கு 21 காசுகள் என்பது பல பயன்பாடுகளுக்கு மிகவும் விலை உயர்ந்தது.

இந்தப் பிரச்சனையைத் தீர்க்க, Ethereum சுற்றுச்சூழல் அமைப்பு பரவலாக்கப்பட்ட முறையில் தரவைச் சேமிக்க பல மாற்று வழிகளை உருவாக்கியது. வழக்கமாக அவை கிடைக்கும் தன்மைக்கும் விலைக்கும் இடையே ஒரு சமரசத்தைக் கொண்டிருக்கும். எனினும், ஒருமைப்பாடு வழக்கமாக உறுதி செய்யப்படுகிறது.

இந்தக் கட்டுரையில், மெர்க்கல் சான்றுகளைப் (opens in a new tab) பயன்படுத்தி, பிளாக்செயினில் தரவைச் சேமிக்காமல் தரவு ஒருமைப்பாட்டை எவ்வாறு உறுதி செய்வது என்பதை நீங்கள் அறிந்து கொள்கிறீர்கள்.

இது எவ்வாறு வேலை செய்கிறது?

கோட்பாட்டளவில், நாம் தரவின் ஹாஷை ஆன்செயினில் சேமித்து, தேவைப்படும் பரிவர்த்தனைகளில் அனைத்து தரவையும் அனுப்பலாம். இருப்பினும், இது இன்னும் மிகவும் விலை உயர்ந்தது. ஒரு பரிவர்த்தனைக்கு ஒரு பைட் தரவுக்கு சுமார் 16 எரிவாயு செலவாகும், தற்போது இது சுமார் அரை சென்ட் அல்லது ஒரு கிலோபைட்டுக்கு சுமார் $5 ஆகும். ஒரு மெகாபைட்டிற்கு $5000 என்பது, தரவை ஹாஷிங் செய்வதற்கான கூடுதல் செலவு இல்லாமல் கூட, பல பயன்பாடுகளுக்கு மிகவும் விலை உயர்ந்தது.

இதற்கான தீர்வு, தரவின் வெவ்வேறு துணைக்குழுக்களை மீண்டும் மீண்டும் ஹாஷ் செய்வதாகும், எனவே நீங்கள் அனுப்பத் தேவையில்லாத தரவுகளுக்கு ஒரு ஹாஷை அனுப்பினால் போதும். இதை நீங்கள் ஒரு மெர்க்கல் மரத்தைப் பயன்படுத்தி செய்கிறீர்கள், இது ஒரு மரத் தரவு அமைப்பாகும், இதில் ஒவ்வொரு முனையும் அதற்குக் கீழே உள்ள முனைகளின் ஹாஷ் ஆகும்:

மெர்க்கல் மரம்

ரூட் ஹாஷ் மட்டுமே ஆன்செயினில் சேமிக்கப்பட வேண்டிய ஒரே பகுதியாகும். ஒரு குறிப்பிட்ட மதிப்பை நிரூபிக்க, ரூட்டைப் பெறுவதற்கு அதனுடன் இணைக்கப்பட வேண்டிய அனைத்து ஹாஷ்களையும் நீங்கள் வழங்க வேண்டும். எடுத்துக்காட்டாக, C என்பதை நிரூபிக்க, நீங்கள் D, H(A-B), மற்றும் H(E-H) ஆகியவற்றை வழங்குகிறீர்கள்.

C இன் மதிப்புக்கான சான்று

செயல்படுத்துதல்

மாதிரிக் குறியீடு இங்கே வழங்கப்பட்டுள்ளது (opens in a new tab).

ஆஃப்செயின் குறியீடு

இந்தக் கட்டுரையில் ஆஃப்செயின் கணக்கீடுகளுக்கு ஜாவாஸ்கிரிப்டைப் பயன்படுத்துகிறோம். பெரும்பாலான பரவலாக்கப்பட்ட பயன்பாடுகள் தங்கள் ஆஃப்செயின் கூறுகளை ஜாவாஸ்கிரிப்டில் கொண்டுள்ளன.

மெர்க்கல் ரூட்டை உருவாக்குதல்

முதலில் நாம் மெர்க்கல் ரூட்டை செயினுக்கு வழங்க வேண்டும்.

1const ethers = require("ethers")

ஈதர்ஸ் தொகுப்பிலிருந்து ஹாஷ் செயல்பாட்டை நாங்கள் பயன்படுத்துகிறோம் (opens in a new tab).

1// நாம் சரிபார்க்க வேண்டிய மூலத் தரவின் ஒருமைப்பாடு. முதல் இரண்டு பைட்டுகள் a
2// ஒரு பயனர் அடையாளங்காட்டி, மற்றும் கடைசி இரண்டு பைட்டுகள் பயனர்
3// தற்போது வைத்திருக்கும் டோக்கன்களின் அளவு.
4const dataArray = [
5 0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,
6 0xface0070, 0xbad00080, 0x060d0091,
7]

ஒவ்வொரு உள்ளீட்டையும் ஒற்றை 256-பிட் முழு எண்ணாக குறியாக்கம் செய்வது, எடுத்துக்காட்டாக, JSON ஐப் பயன்படுத்துவதை விட குறைவான படிக்கக்கூடிய குறியீட்டை விளைவிக்கும். இருப்பினும், இது ஒப்பந்தத்தில் உள்ள தரவை மீட்டெடுப்பதற்கு கணிசமாக குறைவான செயலாக்கத்தைக் குறிக்கிறது, எனவே மிகக் குறைந்த எரிவாயு செலவுகள். நீங்கள் JSON-ஐ ஆன்செயினில் படிக்கலாம் (opens in a new tab), தவிர்க்க முடிந்தால் அது ஒரு மோசமான யோசனை.

1// ஹாஷ் மதிப்புகளின் வரிசை, BigInts ஆக
2const hashArray = dataArray

இந்த நிலையில் நமது தரவு தொடங்குவதற்கு 256-பிட் மதிப்புகளாகும், எனவே செயலாக்கம் எதுவும் தேவையில்லை. சரங்கள் போன்ற மிகவும் சிக்கலான தரவு கட்டமைப்பைப் பயன்படுத்தினால், ஹாஷ்களின் வரிசையைப் பெற, முதலில் தரவை ஹாஷ் செய்வதை உறுதிசெய்ய வேண்டும். பயனர்கள் மற்ற பயனர்களின் தகவல்களை அறிந்தால் நாங்கள் கவலைப்படுவதில்லை என்பதையும் கவனத்தில் கொள்க. இல்லையெனில், பயனர் 1 பயனர் 0-க்கான மதிப்பை அறியாதபடி நாம் ஹாஷ் செய்ய வேண்டியிருக்கும், பயனர் 2 பயனர் 3-க்கான மதிப்பை அறிய மாட்டார், முதலியன.

1// ஹாஷ் செயல்பாடு எதிர்பார்க்கும் சரம் மற்றும்
2// நாம் எல்லா இடங்களிலும் பயன்படுத்தும் BigInt ஆகியவற்றுக்கு இடையே மாற்றவும்.
3const hash = (x) =>
4 BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0)))

ஈதர்ஸ் ஹாஷ் செயல்பாடு, 0x60A7 போன்ற ஹெக்ஸாடெசிமல் எண்ணுடன் கூடிய ஜாவாஸ்கிரிப்ட் சரத்தைப் பெறும் என எதிர்பார்க்கிறது, மேலும் அதே அமைப்புடன் மற்றொரு சரத்துடன் பதிலளிக்கிறது. இருப்பினும், குறியீட்டின் மற்ற பகுதிகளுக்கு BigInt ஐப் பயன்படுத்துவது எளிதானது, எனவே நாங்கள் ஒரு ஹெக்ஸாடெசிமல் சரத்திற்கு மாற்றி மீண்டும் மாற்றுகிறோம்.

1// ஒரு ஜோடியின் சமச்சீர் ஹாஷ், அதனால் வரிசை தலைகீழாக மாறினாலும் நாங்கள் கவலைப்பட மாட்டோம்.
2const pairHash = (a, b) => hash(hash(a) ^ hash(b))

இந்த செயல்பாடு சமச்சீரானது (a xor (opens in a new tab) b இன் ஹாஷ்). இதன் பொருள், மெர்க்கல் சான்றை சரிபார்க்கும்போது, கணக்கிடப்பட்ட மதிப்புக்கு முன் அல்லது பின் சான்றிலிருந்து மதிப்பை வைப்பது பற்றி கவலைப்படத் தேவையில்லை. மெர்க்கல் சான்று சரிபார்ப்பு ஆன்செயினில் செய்யப்படுகிறது, எனவே அங்கு நாம் எவ்வளவு குறைவாக செய்ய வேண்டுமோ அவ்வளவு நல்லது.

எச்சரிக்கை: குறியாக்கவியல் தோற்றத்தை விட கடினமானது. இந்த கட்டுரையின் ஆரம்பப் பதிப்பில் hash(a^b) என்ற ஹாஷ் செயல்பாடு இருந்தது. அது ஒரு மோசமான யோசனையாக இருந்தது, ஏனெனில் a மற்றும் b ஆகியவற்றின் முறையான மதிப்புகள் உங்களுக்குத் தெரிந்தால், விரும்பிய a' மதிப்பை நிரூபிக்க நீங்கள் b' = a^b^a' ஐப் பயன்படுத்தலாம் என்று அது அர்த்தப்படுத்தியது. இந்தச் செயல்பாட்டின் மூலம், hash(a') ^ hash(b') என்பது ஒரு அறியப்பட்ட மதிப்புக்கு (ரூட்டிற்குச் செல்லும் வழியில் உள்ள அடுத்த கிளை) சமமாக இருக்கும் வகையில் b' ஐ நீங்கள் கணக்கிட வேண்டும், இது மிகவும் கடினமானது.

1// ஒரு குறிப்பிட்ட கிளை காலியாக இருப்பதைக் குறிக்கும் மதிப்பு, அதற்கு
2// மதிப்பு இல்லை
3const empty = 0n

மதிப்புகளின் எண்ணிக்கை இரண்டின் முழு எண் அடுக்கு அல்லாதபோது, நாம் வெற்று கிளைகளைக் கையாள வேண்டும். இந்த நிரல் இதைச் செய்யும் வழி, பூஜ்ஜியத்தை ஒரு இடம்காட்டியாக வைப்பதாகும்.

கிளைகள் இல்லாத மெர்க்கல் மரம்

1// வரிசையில் ஒவ்வொரு ஜோடியின் ஹாஷை எடுத்து ஒரு ஹாஷ் வரிசையின் மரத்தை ஒரு நிலை மேலே கணக்கிடவும்
2//
3const oneLevelUp = (inputArray) => {
4 var result = []
5 var inp = [...inputArray] // உள்ளீட்டை மேலெழுதுவதைத் தவிர்க்க // தேவைப்பட்டால் ஒரு வெற்று மதிப்பைச் சேர்க்கவும் (அனைத்து இலைகளும் ஜோடியாக இருக்க வேண்டும் //)
6
7 if (inp.length % 2 === 1) inp.push(empty)
8
9 for (var i = 0; i < inp.length; i += 2)
10 result.push(pairHash(inp[i], inp[i + 1]))
11
12 return result
13} // oneLevelUp
அனைத்தையும் காட்டு

இந்த செயல்பாடு தற்போதைய அடுக்கில் உள்ள மதிப்புகளின் ஜோடிகளை ஹாஷிங் செய்வதன் மூலம் மெர்க்கல் மரத்தில் ஒரு நிலை "ஏறுகிறது". இது மிகவும் திறமையான செயலாக்கம் அல்ல என்பதை நினைவில் கொள்க, உள்ளீட்டை நகலெடுப்பதை நாங்கள் தவிர்த்திருக்கலாம் மற்றும் சுழற்சியில் பொருத்தமான විට hashEmpty ஐ சேர்த்திருக்கலாம், ஆனால் இந்த குறியீடு படிக்க எளிதாக உகந்ததாக்கப்பட்டுள்ளது.

1const getMerkleRoot = (inputArray) => {
2 var result
3
4 result = [...inputArray] // ஒரே ஒரு மதிப்பு இருக்கும் வரை மரத்தில் ஏறுங்கள், அதுதான் // ரூட். // // ஒரு அடுக்கில் ஒற்றைப்படை உள்ளீடுகள் இருந்தால், oneLevelUp இல் உள்ள // குறியீடு ஒரு வெற்று மதிப்பைச் சேர்க்கிறது, எனவே, எடுத்துக்காட்டாக, // 10 இலைகள் இருந்தால், இரண்டாவது அடுக்கில் 5 கிளைகள், மூன்றாவது அடுக்கில் 3 // கிளைகள், நான்காவதில் 2, ஐந்தாவது ரூட் ஆகும்
5
6 while (result.length > 1) result = oneLevelUp(result)
7
8 return result[0]
9}
அனைத்தையும் காட்டு

ரூட்டைப் பெற, ஒரே ஒரு மதிப்பு மட்டுமே இருக்கும் வரை ஏறவும்.

ஒரு மெர்க்கல் சான்றை உருவாக்குதல்

ஒரு மெர்க்கல் சான்று என்பது மெர்க்கல் ரூட்டைத் திரும்பப் பெறுவதற்காக நிரூபிக்கப்படும் மதிப்புடன் சேர்த்து ஹாஷ் செய்ய வேண்டிய மதிப்புகள் ஆகும். நிரூபிக்க வேண்டிய மதிப்பு பெரும்பாலும் மற்ற தரவுகளிலிருந்து கிடைக்கிறது, எனவே அதை குறியீட்டின் ஒரு பகுதியாக வழங்குவதை விட தனித்தனியாக வழங்க விரும்புகிறேன்.

1// ஒரு மெர்க்கல் சான்று, உள்ளீடுகளின் பட்டியலின் மதிப்புடன்
2// ஹாஷ் செய்வதைக் கொண்டுள்ளது. நாம் ஒரு சமச்சீர் ஹாஷ் செயல்பாட்டைப் பயன்படுத்துவதால், சான்றைச் சரிபார்க்க பொருளின் இருப்பிடம்
3// நமக்குத் தேவையில்லை, அதை உருவாக்க மட்டுமே தேவை
4const getMerkleProof = (inputArray, n) => {
5    var result = [], currentLayer = [...inputArray], currentN = n
6
7    // நாம் உச்சியை அடையும் வரை
8    while (currentLayer.length > 1) {
9        // ஒற்றைப்படை நீள அடுக்குகள் இல்லை
10        if (currentLayer.length % 2)
11            currentLayer.push(empty)
12
13        result.push(currentN % 2
14               // currentN ஒற்றைப்படையாக இருந்தால், அதற்கு முந்தைய மதிப்புடன் சான்றுக்குச் சேர்க்கவும்
15            ? currentLayer[currentN-1]
16               // அது இரட்டைப்படையாக இருந்தால், அதற்குப் பின் உள்ள மதிப்பைச் சேர்க்கவும்
17            : currentLayer[currentN+1])
18
அனைத்தையும் காட்டு

நாம் (v[0],v[1]), (v[2],v[3]) போன்றவற்றை ஹாஷ் செய்கிறோம். எனவே இரட்டைப்படை மதிப்புகளுக்கு அடுத்தது தேவை, ஒற்றைப்படை மதிப்புகளுக்கு முந்தையது தேவை.

1        // அடுத்த அடுக்குக்கு மேலே செல்லவும்
2        currentN = Math.floor(currentN/2)
3        currentLayer = oneLevelUp(currentLayer)
4    }   // while currentLayer.length > 1
5
6    return result
7}   // getMerkleProof

ஆன்செயின் குறியீடு

இறுதியாக, சான்றைச் சரிபார்க்கும் குறியீடு எங்களிடம் உள்ளது. ஆன்செயின் குறியீடு Solidity (opens in a new tab) மொழியில் எழுதப்பட்டுள்ளது. எரிவாயு ஒப்பீட்டளவில் விலை உயர்ந்தது என்பதால், இங்கு உகந்ததாக்குதல் மிகவும் முக்கியமானது.

1//SPDX-License-Identifier: பொதுக் களம்
2pragma solidity ^0.8.0;
3
4import "hardhat/console.sol";

நான் இதை Hardhat மேம்பாட்டு சூழலைப் (opens in a new tab) பயன்படுத்தி எழுதினேன், இது உருவாக்கும் போது Solidity-யிலிருந்து கன்சோல் வெளியீட்டை (opens in a new tab) பெற அனுமதிக்கிறது.

1
2contract MerkleProof {
3    uint merkleRoot;
4
5    function getRoot() public view returns (uint) {
6      return merkleRoot;
7    }
8
9    // மிகவும் பாதுகாப்பற்றது, தயாரிப்புக் குறியீட்டில்
10    // இந்தச் செயல்பாட்டிற்கான அணுகல் கண்டிப்பாக வரையறுக்கப்பட வேண்டும், ஒருவேளை ஒரு
11    // உரிமையாளருக்கு
12    function setRoot(uint _merkleRoot) external {
13      merkleRoot = _merkleRoot;
14    }   // setRoot
அனைத்தையும் காட்டு

மெர்க்கல் ரூட்டிற்கான செட் மற்றும் கெட் செயல்பாடுகள். ஒரு தயாரிப்பு அமைப்பில் மெர்க்கல் ரூட்டை அனைவரும் புதுப்பிக்க அனுமதிப்பது மிகவும் மோசமான யோசனை. மாதிரிக் குறியீட்டின் எளிமைக்காக நான் இதை இங்கே செய்கிறேன். தரவு ஒருமைப்பாடு உண்மையில் முக்கியத்துவம் வாய்ந்த ஒரு கணினியில் இதைச் செய்யாதீர்கள்.

1    function hash(uint _a) internal pure returns(uint) {
2      return uint(keccak256(abi.encode(_a)));
3    }
4
5    function pairHash(uint _a, uint _b) internal pure returns(uint) {
6      return hash(hash(_a) ^ hash(_b));
7    }

இந்தச் செயல்பாடு ஒரு ஜோடி ஹாஷை உருவாக்குகிறது. இது hash மற்றும் pairHash க்கான ஜாவாஸ்கிரிப்ட் குறியீட்டின் Solidity மொழிபெயர்ப்பு மட்டுமே.

குறிப்பு: இது படிக்க எளிதாக உகந்ததாக்குதலின் மற்றொரு நிகழ்வு. செயல்பாட்டு வரையறையின் (opens in a new tab) அடிப்படையில், தரவை ஒரு bytes32 (opens in a new tab) மதிப்பாக சேமித்து, மாற்றங்களைத் தவிர்க்க முடியும்.

1    // ஒரு மெர்க்கல் சான்றைச் சரிபார்க்கவும்
2    function verifyProof(uint _value, uint[] calldata _proof)
3        public view returns (bool) {
4      uint temp = _value;
5      uint i;
6
7      for(i=0; i<_proof.length; i++) {
8        temp = pairHash(temp, _proof[i]);
9      }
10
11      return temp == merkleRoot;
12    }
13
14}  // MarkleProof
அனைத்தையும் காட்டு

கணிதக் குறியீட்டில் மெர்க்கல் சான்று சரிபார்ப்பு இப்படி இருக்கும்: H(proof_n, H(proof_n-1, H(proof_n-2, ... H(proof_1, H(proof_0, value))...)))`. இந்தக் குறியீடு அதைச் செயல்படுத்துகிறது.

மெர்க்கல் சான்றுகளும் ரோலப்புகளும் கலப்பதில்லை

மெர்க்கல் சான்றுகள் ரோலப்புகளுடன் நன்றாக வேலை செய்யாது. அதற்குக் காரணம், ரோலப்புகள் அனைத்து பரிவர்த்தனைத் தரவையும் L1 இல் எழுதுகின்றன, ஆனால் L2 இல் செயலாக்குகின்றன. ஒரு பரிவர்த்தனையுடன் ஒரு மெர்க்கல் சான்றை அனுப்புவதற்கான செலவு ஒரு அடுக்குக்கு சராசரியாக 638 எரிவாயு ஆகும் (தற்போது கால் டேட்டாவில் ஒரு பைட் பூஜ்ஜியமாக இல்லாவிட்டால் 16 எரிவாயுவும், பூஜ்ஜியமாக இருந்தால் 4 எரிவாயுவும் செலவாகும்). நம்மிடம் 1024 சொற்கள் தரவு இருந்தால், ஒரு மெர்க்கல் சான்றுக்கு பத்து அடுக்குகள் அல்லது மொத்தம் 6380 எரிவாயு தேவைப்படுகிறது.

எடுத்துக்காட்டாக Optimism (opens in a new tab)-ஐப் பார்த்தால், L1 எரிவாயு எழுதுவதற்கு சுமார் 100 gwei மற்றும் L2 எரிவாயு 0.001 gwei செலவாகும் (அது சாதாரண விலை, நெரிசலுடன் இது உயரக்கூடும்). எனவே ஒரு L1 எரிவாயுவின் விலைக்கு, L2 செயலாக்கத்தில் ஒரு லட்சம் எரிவாயுவை நாம் செலவிடலாம். நாம் சேமிப்பகத்தை மேலெழுதவில்லை என்று வைத்துக் கொண்டால், ஒரு L1 எரிவாயு விலைக்கு L2 இல் உள்ள சேமிப்பகத்தில் சுமார் ஐந்து வார்த்தைகளை எழுதலாம் என்று இது அர்த்தப்படுத்துகிறது. ஒற்றை மெர்க்கல் சான்றுக்கு, முழு 1024 வார்த்தைகளையும் சேமிப்பகத்தில் எழுதலாம் (அவை ஒரு பரிவர்த்தனையில் வழங்கப்படுவதற்குப் பதிலாக, தொடக்கத்தில் ஆன்செயினில் கணக்கிடப்படலாம் என்று கருதி) மேலும் பெரும்பாலான எரிவாயு மீதம் இருக்கும்.

முடிவுரை

நிஜ வாழ்க்கையில் நீங்கள் ஒருபோதும் மெர்க்கல் மரங்களை நீங்களாகவே செயல்படுத்த மாட்டீர்கள். நீங்கள் பயன்படுத்தக்கூடிய நன்கு அறியப்பட்ட மற்றும் தணிக்கை செய்யப்பட்ட நூலகங்கள் உள்ளன, பொதுவாக குறியாக்கவியல் அடிப்படைகளை நீங்களாகவே செயல்படுத்தாமல் இருப்பது நல்லது. ஆனால் இப்போது நீங்கள் மெர்க்கல் சான்றுகளை நன்கு புரிந்துகொண்டு, அவற்றை எப்போது பயன்படுத்த வேண்டும் என்பதைத் தீர்மானிக்க முடியும் என்று நம்புகிறேன்.

மெர்க்கல் சான்றுகள் ஒருமைப்பாட்டைப் பாதுகாத்தாலும், அவை கிடைக்கும் தன்மையைப் பாதுகாக்காது என்பதை நினைவில் கொள்க. தரவுச் சேமிப்பகம் அணுகலை அனுமதிக்க மறுத்து, அவற்றை அணுகுவதற்கு உங்களால் ஒரு மெர்க்கல் மரத்தை உருவாக்க முடியாவிட்டால், வேறு யாரும் உங்கள் சொத்துக்களை எடுக்க முடியாது என்பதை அறிவது ஒரு சிறிய ஆறுதல். எனவே, IPFS போன்ற சில வகையான பரவலாக்கப்பட்ட சேமிப்பகத்துடன் மெர்க்கல் மரங்களைப் பயன்படுத்துவது சிறந்தது.

எனது மேலும் பணிகளை இங்கே பார்க்கவும் (opens in a new tab).

பக்கத்தின் கடைசி புதுப்பிப்பு: 18 டிசம்பர், 2025

இந்தப் பயிற்சி உதவியாக இருந்ததா?