diff --git a/tests/incentivization/test_all.nim b/tests/incentivization/test_all.nim index 3efdc7d6e..756db896d 100644 --- a/tests/incentivization/test_all.nim +++ b/tests/incentivization/test_all.nim @@ -1 +1 @@ -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 000000000..91c030cea --- /dev/null +++ b/tests/incentivization/test_poc.nim @@ -0,0 +1,198 @@ +{.used.} + +import + std/[options], + testutils/unittests, + chronos, + web3, + stew/byteutils, + stint, + strutils, + tests/testlib/testasync + +import + waku/[node/peer_manager, waku_core], + waku/incentivization/[rpc, eligibility_manager], + ../waku_rln_relay/[utils_onchain, utils] + +const TxHashNonExisting = + TxHash.fromHex("0x0000000000000000000000000000000000000000000000000000000000000000") + +# Anvil RPC URL +const EthClient = "ws://127.0.0.1:8540" + +const TxValueExpectedWei = 1000.u256 + +## Storage.sol contract from https://remix.ethereum.org/ +## Compiled with Solidity compiler version "0.8.26+commit.8a97fa7a" + +const ExampleStorageContractBytecode = + "6080604052348015600e575f80fd5b506101438061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80632e64cec1146100385780636057361d14610056575b5f80fd5b610040610072565b60405161004d919061009b565b60405180910390f35b610070600480360381019061006b91906100e2565b61007a565b005b5f8054905090565b805f8190555050565b5f819050919050565b61009581610083565b82525050565b5f6020820190506100ae5f83018461008c565b92915050565b5f80fd5b6100c181610083565b81146100cb575f80fd5b50565b5f813590506100dc816100b8565b92915050565b5f602082840312156100f7576100f66100b4565b5b5f610104848285016100ce565b9150509291505056fea26469706673582212209a0dd35336aff1eb3eeb11db76aa60a1427a12c1b92f945ea8c8d1dfa337cf2264736f6c634300081a0033" + +contract(ExampleStorageContract): + proc number(): UInt256 {.view.} + proc store(num: UInt256) + proc retrieve(): UInt256 {.view.} + +#[ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.8.2 <0.9.0; + +/** + * @title Storage + * @dev Store & retrieve value in a variable + * @custom:dev-run-script ./scripts/deploy_with_ethers.ts + */ +contract Storage { + + uint256 number; + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} +]# + +proc setup( + manager: EligibilityManager +): Future[(TxHash, TxHash, TxHash, TxHash, TxHash, Address, Address)] {.async.} = + ## Populate the local chain (connected to via manager) + ## with txs required for eligibility testing. + ## + ## 1. Depoly a dummy contract that has a publicly callable function. + ## (While doing so, we confirm a contract creation tx.) + ## 2. Confirm these transactions: + ## - a contract call tx (eligibility test must fail) + ## - a simple transfer with the wrong receiver (must fail) + ## - a simple transfer with the wrong amount (must fail) + ## - a simple transfer with the right receiver and amount (must pass) + + let web3 = manager.web3 + + let accounts = await web3.provider.eth_accounts() + web3.defaultAccount = accounts[0] + let sender = web3.defaultAccount + let receiverExpected = accounts[1] + let receiverNotExpected = accounts[2] + + let txValueEthExpected = TxValueExpectedWei + let txValueEthNotExpected = txValueEthExpected + 1 + + # wrong receiver, wrong amount + let txHashWrongReceiverRightAmount = + await web3.sendEthTransfer(sender, receiverNotExpected, txValueEthExpected) + + # right receiver, wrong amount + let txHashRightReceiverWrongAmount = + await web3.sendEthTransfer(sender, receiverExpected, txValueEthNotExpected) + + # right receiver, right amount + let txHashRightReceiverRightAmount = + await web3.sendEthTransfer(sender, receiverExpected, txValueEthExpected) + + let receipt = await web3.deployContract(ExampleStorageContractBytecode) + let txHashContractCreation = receipt.transactionHash + let exampleStorageContractAddress = receipt.contractAddress.get() + let exampleStorageContract = + web3.contractSender(ExampleStorageContract, exampleStorageContractAddress) + + let txHashContractCall = await exampleStorageContract.store(1.u256).send() + + return ( + txHashWrongReceiverRightAmount, txHashRightReceiverWrongAmount, + txHashRightReceiverRightAmount, txHashContractCreation, txHashContractCall, + receiverExpected, receiverNotExpected, + ) + +suite "Waku Incentivization PoC Eligibility Proofs": + ## Tests for service incentivization PoC. + ## In a client-server interaction, a client submits an eligibility proof to the server. + ## The server provides the service if and only if the proof is valid. + ## In PoC, a txid serves as eligibility proof. + ## The txid reflects the confirmed payment from the client to the server. + ## The request is eligible if the tx is confirmed and pays the correct amount to the correct address. + ## The tx must also be of a "simple transfer" type (not a contract creation, not a contract call). + ## See spec: https://github.com/waku-org/specs/blob/master/standards/core/incentivization.md + + ## Start Anvil + let runAnvil {.used.} = runAnvil() + + var txHashWrongReceiverRightAmount, txHashRightReceiverWrongAmount, + txHashRightReceiverRightAmount, txHashContractCreation, txHashContractCall: TxHash + + var receiverExpected, receiverNotExpected: Address + + var manager {.threadvar.}: EligibilityManager + + asyncSetup: + manager = await EligibilityManager.init(EthClient) + + ( + txHashWrongReceiverRightAmount, txHashRightReceiverWrongAmount, + txHashRightReceiverRightAmount, txHashContractCreation, txHashContractCall, + receiverExpected, receiverNotExpected, + ) = await manager.setup() + + asyncTeardown: + await manager.close() + + asyncTest "incentivization PoC: non-existent tx is not eligible": + ## Test that an unconfirmed tx is not eligible. + + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(TxHashNonExisting.bytes()))) + let isEligible = await manager.isEligibleTxId( + eligibilityProof, receiverExpected, TxValueExpectedWei + ) + check: + isEligible.isErr() + + asyncTest "incentivization PoC: contract creation tx is not eligible": + ## Test that a contract creation tx is not eligible. + + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(txHashContractCreation.bytes()))) + let isEligible = await manager.isEligibleTxId( + eligibilityProof, receiverExpected, TxValueExpectedWei + ) + check: + isEligible.isErr() + + asyncTest "incentivization PoC: contract call tx is not eligible": + ## Test that a contract call tx is not eligible. + ## This assumes a payment in native currency (ETH), not a token. + + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(txHashContractCall.bytes()))) + let isEligible = await manager.isEligibleTxId( + eligibilityProof, receiverExpected, TxValueExpectedWei + ) + check: + isEligible.isErr() + + asyncTest "incentivization PoC: simple transfer tx is eligible": + ## Test that a simple transfer tx is eligible (if necessary conditions hold). + + let eligibilityProof = + EligibilityProof(proofOfPayment: some(@(txHashRightReceiverRightAmount.bytes()))) + let isEligible = await manager.isEligibleTxId( + eligibilityProof, receiverExpected, TxValueExpectedWei + ) + + assert isEligible.isOk(), isEligible.error + + # Stop Anvil daemon + stopAnvil(runAnvil) diff --git a/tests/incentivization/test_rpc_codec.nim b/tests/incentivization/test_rpc_codec.nim index 803796dbd..30befd8c1 100644 --- a/tests/incentivization/test_rpc_codec.nim +++ b/tests/incentivization/test_rpc_codec.nim @@ -1,25 +1,22 @@ -import - std/options, - std/strscans, - testutils/unittests, - chronicles, - chronos, - libp2p/crypto/crypto +import std/options, testutils/unittests, chronos, libp2p/crypto/crypto, web3 -import waku/incentivization/rpc, waku/incentivization/rpc_codec +import waku/incentivization/[rpc, rpc_codec, common] 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 = init(EligibilityStatus, true) + let encoded = encode(eligibilityStatus) let decoded = EligibilityStatus.decode(encoded.buffer).get() check: - esRpc == decoded + eligibilityStatus == decoded diff --git a/tests/node/test_wakunode_relay_rln.nim b/tests/node/test_wakunode_relay_rln.nim index 1304575c7..0bf608d12 100644 --- a/tests/node/test_wakunode_relay_rln.nim +++ b/tests/node/test_wakunode_relay_rln.nim @@ -607,7 +607,7 @@ suite "Waku RlnRelay - End to End - OnChain": asyncTest "Not enough gas": let - onChainGroupManager = await setup(ethAmount = 0.u256) + onChainGroupManager = await setupOnchainGroupManager(amountWei = 0.u256) contractAddress = onChainGroupManager.ethContractAddress keystorePath = genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract") diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 5ef6913f7..7b4d6dbfe 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -11,7 +11,8 @@ import stint, web3, libp2p/crypto/crypto, - eth/keys + eth/keys, + tests/testlib/testasync import waku/[ @@ -33,8 +34,15 @@ suite "Onchain group manager": # We run Anvil let runAnvil {.used.} = runAnvil() + var manager {.threadvar.}: OnchainGroupManager + + asyncSetup: + manager = await setupOnchainGroupManager() + + asyncTeardown: + await manager.stop() + asyncTest "should initialize successfully": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error @@ -45,24 +53,19 @@ suite "Onchain group manager": manager.rlnContractDeployedBlockNumber > 0 manager.rlnRelayMaxMessageLimit == 100 - await manager.stop() - asyncTest "should error on initialization when chainId does not match": - let manager = await setup() manager.chainId = CHAIN_ID + 1 (await manager.init()).isErrOr: raiseAssert "Expected error when chainId does not match" asyncTest "should initialize when chainId is set to 0": - let manager = await setup() manager.chainId = 0 (await manager.init()).isOkOr: raiseAssert $error asyncTest "should error on initialization when loaded metadata does not match": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error @@ -77,8 +80,6 @@ suite "Onchain group manager": assert metadata.contractAddress == manager.ethContractAddress, "contractAddress is not equal to " & manager.ethContractAddress - await manager.stop() - let differentContractAddress = await uploadRLNContract(manager.ethClientUrl) # simulating a change in the contractAddress let manager2 = OnchainGroupManager( @@ -101,7 +102,6 @@ suite "Onchain group manager": asyncTest "should error if contract does not exist": var triggeredError = false - let manager = await setup() manager.ethContractAddress = "0x0000000000000000000000000000000000000000" manager.onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} = echo "---" @@ -116,7 +116,6 @@ suite "Onchain group manager": check triggeredError asyncTest "should error when keystore path and password are provided but file doesn't exist": - let manager = await setup() manager.keystorePath = some("/inexistent/file") manager.keystorePassword = some("password") @@ -124,25 +123,17 @@ suite "Onchain group manager": raiseAssert "Expected error when keystore file doesn't exist" asyncTest "startGroupSync: should start group sync": - let manager = await setup() - (await manager.init()).isOkOr: raiseAssert $error (await manager.startGroupSync()).isOkOr: raiseAssert $error - await manager.stop() asyncTest "startGroupSync: should guard against uninitialized state": - let manager = await setup() - (await manager.startGroupSync()).isErrOr: raiseAssert "Expected error when not initialized" - await manager.stop() - asyncTest "startGroupSync: should sync to the state of the group": - let manager = await setup() let credentials = generateCredentials(manager.rlnInstance) let rateCommitment = getRateCommitment(credentials, UserMessageLimit(1)).valueOr: raiseAssert $error @@ -182,10 +173,8 @@ suite "Onchain group manager": check: metadataOpt.get().validRoots == manager.validRoots.toSeq() merkleRootBefore != merkleRootAfter - await manager.stop() asyncTest "startGroupSync: should fetch history correctly": - let manager = await setup() const credentialCount = 6 let credentials = generateCredentials(manager.rlnInstance, credentialCount) (await manager.init()).isOkOr: @@ -231,10 +220,8 @@ suite "Onchain group manager": check: merkleRootBefore != merkleRootAfter manager.validRootBuffer.len() == credentialCount - AcceptableRootWindowSize - await manager.stop() asyncTest "register: should guard against uninitialized state": - let manager = await setup() let dummyCommitment = default(IDCommitment) try: @@ -248,10 +235,7 @@ suite "Onchain group manager": except Exception: assert false, "exception raised: " & getCurrentExceptionMsg() - await manager.stop() - asyncTest "register: should register successfully": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error (await manager.startGroupSync()).isOkOr: @@ -276,11 +260,8 @@ suite "Onchain group manager": check: merkleRootAfter.inHex() != merkleRootBefore.inHex() manager.latestIndex == 1 - await manager.stop() asyncTest "register: callback is called": - let manager = await setup() - let idCredentials = generateCredentials(manager.rlnInstance) let idCommitment = idCredentials.idCommitment @@ -310,10 +291,7 @@ suite "Onchain group manager": await fut - await manager.stop() - asyncTest "withdraw: should guard against uninitialized state": - let manager = await setup() let idSecretHash = generateCredentials(manager.rlnInstance).idSecretHash try: @@ -323,10 +301,7 @@ suite "Onchain group manager": except Exception: assert false, "exception raised: " & getCurrentExceptionMsg() - await manager.stop() - asyncTest "validateRoot: should validate good root": - let manager = await setup() let credentials = generateCredentials(manager.rlnInstance) (await manager.init()).isOkOr: raiseAssert $error @@ -372,10 +347,8 @@ suite "Onchain group manager": check: validated - await manager.stop() asyncTest "validateRoot: should reject bad root": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error (await manager.startGroupSync()).isOkOr: @@ -405,10 +378,8 @@ suite "Onchain group manager": check: validated == false - await manager.stop() asyncTest "verifyProof: should verify valid proof": - let manager = await setup() let credentials = generateCredentials(manager.rlnInstance) (await manager.init()).isOkOr: raiseAssert $error @@ -451,10 +422,8 @@ suite "Onchain group manager": check: verified - await manager.stop() asyncTest "verifyProof: should reject invalid proof": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error (await manager.startGroupSync()).isOkOr: @@ -500,10 +469,8 @@ suite "Onchain group manager": check: verified == false - await manager.stop() asyncTest "backfillRootQueue: should backfill roots in event of chain reorg": - let manager = await setup() const credentialCount = 6 let credentials = generateCredentials(manager.rlnInstance, credentialCount) (await manager.init()).isOkOr: @@ -557,10 +524,8 @@ suite "Onchain group manager": manager.validRoots.len() == credentialCount - 1 manager.validRootBuffer.len() == 0 manager.validRoots[credentialCount - 2] == expectedLastRoot - await manager.stop() asyncTest "isReady should return false if ethRpc is none": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error @@ -575,10 +540,7 @@ suite "Onchain group manager": check: isReady == false - await manager.stop() - asyncTest "isReady should return false if lastSeenBlockHead > lastProcessed": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error @@ -591,10 +553,7 @@ suite "Onchain group manager": check: isReady == false - await manager.stop() - asyncTest "isReady should return true if ethRpc is ready": - let manager = await setup() (await manager.init()).isOkOr: raiseAssert $error # node can only be ready after group sync is done @@ -610,8 +569,6 @@ suite "Onchain group manager": check: isReady == true - await manager.stop() - ################################ ## Terminating/removing Anvil ################################ diff --git a/tests/waku_rln_relay/utils.nim b/tests/waku_rln_relay/utils.nim index 548414fb7..4e8b93c41 100644 --- a/tests/waku_rln_relay/utils.nim +++ b/tests/waku_rln_relay/utils.nim @@ -29,6 +29,3 @@ proc deployContract*( let r = await web3.send(tr) return await web3.getMinedTransactionReceipt(r) - -proc ethToWei*(eth: UInt256): UInt256 = - eth * 1000000000000000000.u256 diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 272ddffa6..fd3644a7b 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -12,7 +12,8 @@ import web3, json, libp2p/crypto/crypto, - eth/keys + eth/keys, + results import waku/[ @@ -101,28 +102,43 @@ proc uploadRLNContract*(ethClientAddress: string): Future[Address] {.async.} = return proxyAddress -proc createEthAccount*( - ethAmount: UInt256 = 1000.u256 -): Future[(keys.PrivateKey, Address)] {.async.} = - let web3 = await newWeb3(EthClient) - let accounts = await web3.provider.eth_accounts() - let gasPrice = int(await web3.provider.eth_gasPrice()) - web3.defaultAccount = accounts[0] +proc sendEthTransfer*( + web3: Web3, + accountFrom: Address, + accountTo: Address, + amountWei: UInt256, + accountToBalanceBeforeExpectedWei: Option[UInt256] = none(UInt256), +): Future[TxHash] {.async.} = + let doBalanceAssert = accountToBalanceBeforeExpectedWei.isSome() - let pk = keys.PrivateKey.random(rng[]) - let acc = Address(toCanonicalAddress(pk.toPublicKey())) + if doBalanceAssert: + let balanceBeforeWei = await web3.provider.eth_getBalance(accountTo, "latest") + let balanceBeforeExpectedWei = accountToBalanceBeforeExpectedWei.get() + assert balanceBeforeWei == balanceBeforeExpectedWei, + fmt"Balance is {balanceBeforeWei} but expected {balanceBeforeExpectedWei}" + + let gasPrice = int(await web3.provider.eth_gasPrice()) var tx: EthSend - tx.source = accounts[0] - tx.value = some(ethToWei(ethAmount)) - tx.to = some(acc) + tx.source = accountFrom + tx.to = some(accountTo) + tx.value = some(amountWei) tx.gasPrice = some(gasPrice) - # Send ethAmount to acc - discard await web3.send(tx) - let balance = await web3.provider.eth_getBalance(acc, "latest") - assert balance == ethToWei(ethAmount), - fmt"Balance is {balance} but expected {ethToWei(ethAmount)}" + # TODO: handle the error if sending fails + let txHash = await web3.send(tx) + + if doBalanceAssert: + let balanceAfterWei = await web3.provider.eth_getBalance(accountTo, "latest") + let balanceAfterExpectedWei = accountToBalanceBeforeExpectedWei.get() + amountWei + assert balanceAfterWei == balanceAfterExpectedWei, + fmt"Balance is {balanceAfterWei} but expected {balanceAfterExpectedWei}" + + return txHash + +proc createEthAccount*(web3: Web3): (keys.PrivateKey, Address) = + let pk = keys.PrivateKey.random(rng[]) + let acc = Address(toCanonicalAddress(pk.toPublicKey())) return (pk, acc) @@ -189,8 +205,11 @@ proc stopAnvil*(runAnvil: Process) {.used.} = except: error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg() -proc setup*( - ethClientAddress: string = EthClient, ethAmount: UInt256 = 10.u256 +proc ethToWei(eth: UInt256): UInt256 = + eth * 1000000000000000000.u256 + +proc setupOnchainGroupManager*( + ethClientAddress: string = EthClient, amountEth: UInt256 = 10.u256 ): Future[OnchainGroupManager] {.async.} = let rlnInstanceRes = createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain")) @@ -206,15 +225,19 @@ proc setup*( let accounts = await web3.provider.eth_accounts() web3.defaultAccount = accounts[0] - var pk = none(string) - let (privateKey, _) = await createEthAccount(ethAmount) - pk = some($privateKey) + let (privateKey, acc) = createEthAccount(web3) + + # we just need to fund the default account + # the send procedure returns a tx hash that we don't use, hence discard + discard await sendEthTransfer( + web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256) + ) let manager = OnchainGroupManager( ethClientUrl: ethClientAddress, ethContractAddress: $contractAddress, chainId: CHAIN_ID, - ethPrivateKey: pk, + ethPrivateKey: some($privateKey), rlnInstance: rlnInstance, onFatalErrorAction: proc(errStr: string) = raiseAssert errStr diff --git a/waku/incentivization/common.nim b/waku/incentivization/common.nim new file mode 100644 index 000000000..79fcf1645 --- /dev/null +++ b/waku/incentivization/common.nim @@ -0,0 +1,9 @@ +import std/options + +import waku/incentivization/[rpc, eligibility_manager] + +proc init*(T: type EligibilityStatus, isEligible: bool): T = + if isEligible: + EligibilityStatus(statusCode: uint32(200), statusDesc: some("OK")) + else: + EligibilityStatus(statusCode: uint32(402), statusDesc: some("Payment Required")) diff --git a/waku/incentivization/eligibility_manager.nim b/waku/incentivization/eligibility_manager.nim new file mode 100644 index 000000000..74d676fba --- /dev/null +++ b/waku/incentivization/eligibility_manager.nim @@ -0,0 +1,91 @@ +import std/options, chronos, web3, stew/byteutils, stint, results, chronicles + +import waku/incentivization/rpc, tests/waku_rln_relay/[utils_onchain, utils] + +const SimpleTransferGasUsed = Quantity(21000) +const TxReceiptQueryTimeout = 3.seconds + +type EligibilityManager* = ref object # FIXME: make web3 private? + web3*: Web3 + +# Initialize the eligibilityManager with a web3 instance +proc init*( + T: type EligibilityManager, ethClient: string +): Future[EligibilityManager] {.async.} = + result = EligibilityManager(web3: await newWeb3(ethClient)) + # TODO: handle error if web3 instance is not established + +# Clean up the web3 instance +proc close*(eligibilityManager: EligibilityManager) {.async.} = + await eligibilityManager.web3.close() + +proc getTransactionByHash( + eligibilityManager: EligibilityManager, txHash: TxHash +): Future[TransactionObject] {.async.} = + await eligibilityManager.web3.provider.eth_getTransactionByHash(txHash) + +proc getMinedTransactionReceipt( + eligibilityManager: EligibilityManager, txHash: TxHash +): Future[Result[ReceiptObject, string]] {.async.} = + let txReceipt = eligibilityManager.web3.getMinedTransactionReceipt(txHash) + if (await txReceipt.withTimeout(TxReceiptQueryTimeout)): + return ok(txReceipt.value()) + else: + return err("Timeout on tx receipt query, tx hash: " & $txHash) + +proc getTxAndTxReceipt( + eligibilityManager: EligibilityManager, txHash: TxHash +): Future[Result[(TransactionObject, ReceiptObject), string]] {.async.} = + let txFuture = eligibilityManager.getTransactionByHash(txHash) + let receiptFuture = eligibilityManager.getMinedTransactionReceipt(txHash) + await allFutures(txFuture, receiptFuture) + let tx = txFuture.read() + let txReceipt = receiptFuture.read() + if txReceipt.isErr(): + return err("Cannot get tx receipt: " & txReceipt.error) + return ok((tx, txReceipt.get())) + +proc isEligibleTxId*( + eligibilityManager: EligibilityManager, + eligibilityProof: EligibilityProof, + expectedToAddress: Address, + expectedValueWei: UInt256, +): Future[Result[void, string]] {.async.} = + ## We consider a tx eligible, + ## in the context of service incentivization PoC, + ## if it is confirmed and pays the expected amount to the server's address. + ## See spec: https://github.com/waku-org/specs/blob/master/standards/core/incentivization.md + if eligibilityProof.proofOfPayment.isNone(): + return err("Eligibility proof is empty") + var tx: TransactionObject + var txReceipt: ReceiptObject + let txHash = TxHash.fromHex(byteutils.toHex(eligibilityProof.proofOfPayment.get())) + try: + let txAndTxReceipt = await eligibilityManager.getTxAndTxReceipt(txHash) + txAndTxReceipt.isOkOr: + return err("Failed to fetch tx or tx receipt") + (tx, txReceipt) = txAndTxReceipt.value() + except ValueError: + let errorMsg = "Failed to fetch tx or tx receipt: " & getCurrentExceptionMsg() + error "exception in isEligibleTxId", error = $errorMsg + return err($errorMsg) + # check that it is not a contract creation tx + let toAddressOption = txReceipt.to + if toAddressOption.isNone(): + # this is a contract creation tx + return err("A contract creation tx is not eligible") + # check that it is a simple transfer (not a contract call) + # a simple transfer uses 21000 gas + let gasUsed = txReceipt.gasUsed + let isSimpleTransferTx = (gasUsed == SimpleTransferGasUsed) + if not isSimpleTransferTx: + return err("A contract call tx is not eligible") + # check that the to address is "as expected" + let toAddress = toAddressOption.get() + if toAddress != expectedToAddress: + return err("Wrong destination address: " & $toAddress) + # check that the amount is "as expected" + let txValueWei = tx.value + if txValueWei != expectedValueWei: + return err("Wrong tx value: got " & $txValueWei & ", expected " & $expectedValueWei) + return ok() diff --git a/waku/incentivization/rpc.nim b/waku/incentivization/rpc.nim index d16ca75a8..5223f5b5b 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