From 13144bc4d3f1ab33edbe71b57276860d6a5b985d Mon Sep 17 00:00:00 2001 From: Sergei Tikhomirov Date: Thu, 31 Oct 2024 12:20:07 +0100 Subject: [PATCH] feat: add simple txid-based eligibility check with hard-coded params (#3166) --- tests/incentivization/test_all.nim | 4 +- tests/incentivization/test_poc.nim | 59 ++++++++++++++++++++++ tests/incentivization/test_rpc_codec.nim | 36 +++++++++----- waku/incentivization/common.nim | 13 +++++ waku/incentivization/eligibility.nim | 6 +++ waku/incentivization/rpc.nim | 3 +- waku/incentivization/rpc_codec.nim | 2 + waku/incentivization/txid_proof.nim | 62 ++++++++++++++++++++++++ 8 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 tests/incentivization/test_poc.nim create mode 100644 waku/incentivization/common.nim create mode 100644 waku/incentivization/eligibility.nim create mode 100644 waku/incentivization/txid_proof.nim diff --git a/tests/incentivization/test_all.nim b/tests/incentivization/test_all.nim index 3efdc7d6e7..d63dcf502a 100644 --- a/tests/incentivization/test_all.nim +++ b/tests/incentivization/test_all.nim @@ -1 +1,3 @@ -import ./test_rpc_codec +import + ./test_rpc_codec, + ./test_poc diff --git a/tests/incentivization/test_poc.nim b/tests/incentivization/test_poc.nim new file mode 100644 index 0000000000..2228e276f6 --- /dev/null +++ b/tests/incentivization/test_poc.nim @@ -0,0 +1,59 @@ +{.used.} + +import + std/[options, strscans], + testutils/unittests, + chronicles, + chronos, + libp2p/crypto/crypto, + web3 + +import + waku/[node/peer_manager, waku_core], + ../testlib/[assertions, wakucore, testasync, futures, testutils], + waku/incentivization/[rpc, rpc_codec, common, txid_proof, eligibility] + +# All txids from Ethereum Sepolia testnet +const TxHashNonExisting* = + TxHash.fromHex("0x0000000000000000000000000000000000000000000000000000000000000000") +const TxHashContractCreation* = + TxHash.fromHex("0xa2e39bee557144591fb7b2891ef44e1392f86c5ba1fc0afb6c0e862676ffd50f") +const TxHashContractCall* = + TxHash.fromHex("0x2761f066eeae9a259a0247f529133dd01b7f57bf74254a64d897433397d321cb") +const TxHashSimpleTransfer* = + TxHash.fromHex("0xa3985984b2ec3f1c3d473eb57a4820a56748f25dabbf9414f2b8380312b439cc") + +const EthClient = "https://sepolia.infura.io/v3/470c2e9a16f24057aee6660081729fb9" + +suite "Waku Incentivization PoC Eligibility Proofs": + + asyncTest "incentivization PoC: non-existent tx is not eligible": + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(TxHashNonExisting.bytes()))) + let txIsEligible = await isEligible(eligibilityProof, EthClient) + check: + not txIsEligible + + asyncTest "incentivization PoC: contract creation tx is not eligible": + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(TxHashContractCreation.bytes()))) + let txIsEligible = await isEligible(eligibilityProof, EthClient) + check: + not txIsEligible + + asyncTest "incentivization PoC: contract call tx is not eligible": + # note: assuming payment in native currency (ETH), not a token + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(TxHashContractCall.bytes()))) + let txIsEligible = await isEligible(eligibilityProof, EthClient) + check: + not txIsEligible + + asyncTest "incentivization PoC: simple transfer tx is eligible": + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(TxHashSimpleTransfer.bytes()))) + let txIdExists = await isEligible(eligibilityProof, EthClient) + check: + txIdExists + + # TODO: add tests for simple transfer txs with wrong amount and wrong receiver diff --git a/tests/incentivization/test_rpc_codec.nim b/tests/incentivization/test_rpc_codec.nim index 803796dbd5..3d46e52f03 100644 --- a/tests/incentivization/test_rpc_codec.nim +++ b/tests/incentivization/test_rpc_codec.nim @@ -1,25 +1,35 @@ import std/options, - std/strscans, testutils/unittests, - chronicles, chronos, - libp2p/crypto/crypto + libp2p/crypto/crypto, + web3 + +import + waku/incentivization/[ + rpc, + rpc_codec, + common + ] -import waku/incentivization/rpc, waku/incentivization/rpc_codec suite "Waku Incentivization Eligibility Codec": - asyncTest "encode eligibility proof": - var byteSequence: seq[byte] = @[1, 2, 3, 4, 5, 6, 7, 8] - let epRpc = EligibilityProof(proofOfPayment: some(byteSequence)) - let encoded = encode(epRpc) + + asyncTest "encode eligibility proof from txid": + let txHash = TxHash.fromHex( + "0x0000000000000000000000000000000000000000000000000000000000000000") + let txHashAsBytes = @(txHash.bytes()) + let eligibilityProof = EligibilityProof(proofOfPayment: some(txHashAsBytes)) + let encoded = encode(eligibilityProof) let decoded = EligibilityProof.decode(encoded.buffer).get() check: - epRpc == decoded - + eligibilityProof == decoded + asyncTest "encode eligibility status": - let esRpc = EligibilityStatus(statusCode: uint32(200), statusDesc: some("OK")) - let encoded = encode(esRpc) + let eligibilityStatus = genEligibilityStatus(true) + let encoded = encode(eligibilityStatus) let decoded = EligibilityStatus.decode(encoded.buffer).get() check: - esRpc == decoded + eligibilityStatus == decoded + + diff --git a/waku/incentivization/common.nim b/waku/incentivization/common.nim new file mode 100644 index 0000000000..6f376e4bc0 --- /dev/null +++ b/waku/incentivization/common.nim @@ -0,0 +1,13 @@ +import std/options + +import waku/incentivization/rpc + +proc genEligibilityStatus*(isEligible: bool): EligibilityStatus = + if isEligible: + EligibilityStatus( + statusCode: uint32(200), + statusDesc: some("OK")) + else: + EligibilityStatus( + statusCode: uint32(402), + statusDesc: some("Payment Required")) \ No newline at end of file diff --git a/waku/incentivization/eligibility.nim b/waku/incentivization/eligibility.nim new file mode 100644 index 0000000000..915c0b8a04 --- /dev/null +++ b/waku/incentivization/eligibility.nim @@ -0,0 +1,6 @@ +import std/options, chronos + +import waku/incentivization/[rpc, txid_proof] + +proc isEligible*(eligibilityProof: EligibilityProof, ethClient: string): Future[bool] {.async.} = + result = await txidEligiblityCriteriaMet(eligibilityProof, ethClient) diff --git a/waku/incentivization/rpc.nim b/waku/incentivization/rpc.nim index d16ca75a8d..5223f5b5b3 100644 --- a/waku/incentivization/rpc.nim +++ b/waku/incentivization/rpc.nim @@ -1,5 +1,4 @@ -import json_serialization, std/options -import ../waku_core +import std/options # Implementing the RFC: # https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/73 diff --git a/waku/incentivization/rpc_codec.nim b/waku/incentivization/rpc_codec.nim index 5d3ce48d53..e06180c431 100644 --- a/waku/incentivization/rpc_codec.nim +++ b/waku/incentivization/rpc_codec.nim @@ -1,6 +1,8 @@ import std/options import ../common/protobuf, ../waku_core, ./rpc +const DefaultMaxRpcSize* = -1 + # Codec for EligibilityProof proc encode*(epRpc: EligibilityProof): ProtoBuffer = diff --git a/waku/incentivization/txid_proof.nim b/waku/incentivization/txid_proof.nim new file mode 100644 index 0000000000..b856bb405a --- /dev/null +++ b/waku/incentivization/txid_proof.nim @@ -0,0 +1,62 @@ +import std/options, chronos, web3, stew/byteutils, stint, strutils + +import waku/incentivization/rpc + + +# Function to convert a hex string to Address +proc toAddress*(hexStr: string): Address = + # Remove the "0x" prefix if it exists + let cleaned = if hexStr.startsWith("0x"): hexStr[2..^1] else: hexStr + + # Ensure the length is exactly 40 characters (20 bytes) + if cleaned.len != 40: + raise newException(ValueError, "Invalid hexadecimal string length for Address") + + var arr: array[20, byte] + for i in 0 ..< 20: + let byteValue = cleaned[i * 2 ..< i * 2 + 2] # Get two hex characters + arr[i] = byte(parseHexInt(byteValue)) + + result = Address(arr) + + +proc checkTxIdIsEligible(txHash: TxHash, ethClient: string): Future[bool] {.async.} = + let web3 = await newWeb3(ethClient) + try: + let tx = await web3.provider.eth_getTransactionByHash(txHash) + let txReceipt = await web3.getMinedTransactionReceipt(txHash) + result = true + if result: + # check that it is not a contract creation tx + let toAddressOption = txReceipt.to + let isContractCreationTx = toAddressOption.isNone + if isContractCreationTx: + result = false + else: + # check that it is a simple transfer (not a contract call) + # a simple transfer uses 21000 gas + let gasUsed = txReceipt.gasUsed + let isSimpleTransferTx = (gasUsed == Quantity(21000)) + if not isSimpleTransferTx: + result = false + else: + # check that the amount is "as expected" (hard-coded for now) + let txValue = tx.value + let hasExpectedValue = (txValue == 200500000000005063.u256) + # check that the to address is "as expected" (hard-coded for now) + let toAddress = toAddressOption.get() + let hasExpectedToAddress = (toAddress == toAddress("0x5e809a85aa182a9921edd10a4163745bb3e36284")) + result = true + except ValueError as e: + result = false + await web3.close() + result + +proc txidEligiblityCriteriaMet*( + eligibilityProof: EligibilityProof, ethClient: string +): Future[bool] {.async.} = + if eligibilityProof.proofOfPayment.isNone(): + return false + let txHash = TxHash.fromHex(byteutils.toHex(eligibilityProof.proofOfPayment.get())) + let txExists = await checkTxIdIsEligible(txHash, ethClient) + return txExists