feat: add txhash-based eligibility checks for incentivization PoC (#3166)

Implement data structures and tests for checking transaction eligibility based on tx hash. This work will be continues in future PRs. All code added in this PR is only used in tests.

* feat: add simple txid-based eligibility check with hard-coded params (#3166)

* use new proc to generate eligibility status

Co-authored-by: gabrielmer <101006718+gabrielmer@users.noreply.github.com>

* minor fixes

* add comments to clarify eligibility definition

* use Address.fromHex conversion from eth-web3

* move isEligible to common

* refactor: avoid result and unnecesary branching

* define const for simple transfer gas usage

* avoid unnecessary parentheses

* chore: run nph linter manually

* refactor, move all hard-coded constants to tests

* use Result type in eligibility tests

* use standard method of error handling

* make try-block smaller

* add a try-block in case of connection failure to web3 provider

* make queries to web3 provider in parallel

* move Web3 provider RPC URL into env variable

* remove unused import

* rename functions

* use await in async proc

Co-authored-by: gabrielmer <101006718+gabrielmer@users.noreply.github.com>

* add timeout to tx receipt query

* parallelize queries for tx and txreceipt

* make test txids non public

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* use assert in txid i13n test

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* use parentheses when calling verb-methods without arguments

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* remove unused import

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* use init for stack-allocated objects

* add txReceipt error message to error

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* introduce eligibility manager

* [WIP] use Anvil for eligibility testing

* add eligibility test with contract deployment tx

* add eligibility test with contract call

* add asyncSetup and asyncTeardown for eligibility tests

* minor refactor

* refactor tests for onchain group manager with asyncSetup and asyncTeardown

* minor refactor

* remove unnecessary defer in asyncTeardown

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* remove unnecessary call in test (moved to asyncTeardown)

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* add comment justidying the use of discard

* rename file txid_proof to eligibility_manager

---------

Co-authored-by: gabrielmer <101006718+gabrielmer@users.noreply.github.com>
Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>
This commit is contained in:
Sergei Tikhomirov 2025-01-22 11:16:49 +01:00 committed by GitHub
parent dd1a70bdb7
commit fdfc48c923
10 changed files with 371 additions and 100 deletions

View File

@ -1 +1 @@
import ./test_rpc_codec import ./test_rpc_codec, ./test_poc

View File

@ -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)

View File

@ -1,25 +1,22 @@
import import std/options, testutils/unittests, chronos, libp2p/crypto/crypto, web3
std/options,
std/strscans,
testutils/unittests,
chronicles,
chronos,
libp2p/crypto/crypto
import waku/incentivization/rpc, waku/incentivization/rpc_codec import waku/incentivization/[rpc, rpc_codec, common]
suite "Waku Incentivization Eligibility Codec": suite "Waku Incentivization Eligibility Codec":
asyncTest "encode eligibility proof": asyncTest "encode eligibility proof from txid":
var byteSequence: seq[byte] = @[1, 2, 3, 4, 5, 6, 7, 8] let txHash = TxHash.fromHex(
let epRpc = EligibilityProof(proofOfPayment: some(byteSequence)) "0x0000000000000000000000000000000000000000000000000000000000000000"
let encoded = encode(epRpc) )
let txHashAsBytes = @(txHash.bytes())
let eligibilityProof = EligibilityProof(proofOfPayment: some(txHashAsBytes))
let encoded = encode(eligibilityProof)
let decoded = EligibilityProof.decode(encoded.buffer).get() let decoded = EligibilityProof.decode(encoded.buffer).get()
check: check:
epRpc == decoded eligibilityProof == decoded
asyncTest "encode eligibility status": asyncTest "encode eligibility status":
let esRpc = EligibilityStatus(statusCode: uint32(200), statusDesc: some("OK")) let eligibilityStatus = init(EligibilityStatus, true)
let encoded = encode(esRpc) let encoded = encode(eligibilityStatus)
let decoded = EligibilityStatus.decode(encoded.buffer).get() let decoded = EligibilityStatus.decode(encoded.buffer).get()
check: check:
esRpc == decoded eligibilityStatus == decoded

View File

@ -607,7 +607,7 @@ suite "Waku RlnRelay - End to End - OnChain":
asyncTest "Not enough gas": asyncTest "Not enough gas":
let let
onChainGroupManager = await setup(ethAmount = 0.u256) onChainGroupManager = await setupOnchainGroupManager(amountWei = 0.u256)
contractAddress = onChainGroupManager.ethContractAddress contractAddress = onChainGroupManager.ethContractAddress
keystorePath = keystorePath =
genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract") genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract")

View File

@ -11,7 +11,8 @@ import
stint, stint,
web3, web3,
libp2p/crypto/crypto, libp2p/crypto/crypto,
eth/keys eth/keys,
tests/testlib/testasync
import import
waku/[ waku/[
@ -33,8 +34,15 @@ suite "Onchain group manager":
# We run Anvil # We run Anvil
let runAnvil {.used.} = runAnvil() let runAnvil {.used.} = runAnvil()
var manager {.threadvar.}: OnchainGroupManager
asyncSetup:
manager = await setupOnchainGroupManager()
asyncTeardown:
await manager.stop()
asyncTest "should initialize successfully": asyncTest "should initialize successfully":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
@ -45,24 +53,19 @@ suite "Onchain group manager":
manager.rlnContractDeployedBlockNumber > 0 manager.rlnContractDeployedBlockNumber > 0
manager.rlnRelayMaxMessageLimit == 100 manager.rlnRelayMaxMessageLimit == 100
await manager.stop()
asyncTest "should error on initialization when chainId does not match": asyncTest "should error on initialization when chainId does not match":
let manager = await setup()
manager.chainId = CHAIN_ID + 1 manager.chainId = CHAIN_ID + 1
(await manager.init()).isErrOr: (await manager.init()).isErrOr:
raiseAssert "Expected error when chainId does not match" raiseAssert "Expected error when chainId does not match"
asyncTest "should initialize when chainId is set to 0": asyncTest "should initialize when chainId is set to 0":
let manager = await setup()
manager.chainId = 0 manager.chainId = 0
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
asyncTest "should error on initialization when loaded metadata does not match": asyncTest "should error on initialization when loaded metadata does not match":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
@ -77,8 +80,6 @@ suite "Onchain group manager":
assert metadata.contractAddress == manager.ethContractAddress, assert metadata.contractAddress == manager.ethContractAddress,
"contractAddress is not equal to " & manager.ethContractAddress "contractAddress is not equal to " & manager.ethContractAddress
await manager.stop()
let differentContractAddress = await uploadRLNContract(manager.ethClientUrl) let differentContractAddress = await uploadRLNContract(manager.ethClientUrl)
# simulating a change in the contractAddress # simulating a change in the contractAddress
let manager2 = OnchainGroupManager( let manager2 = OnchainGroupManager(
@ -101,7 +102,6 @@ suite "Onchain group manager":
asyncTest "should error if contract does not exist": asyncTest "should error if contract does not exist":
var triggeredError = false var triggeredError = false
let manager = await setup()
manager.ethContractAddress = "0x0000000000000000000000000000000000000000" manager.ethContractAddress = "0x0000000000000000000000000000000000000000"
manager.onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} = manager.onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} =
echo "---" echo "---"
@ -116,7 +116,6 @@ suite "Onchain group manager":
check triggeredError check triggeredError
asyncTest "should error when keystore path and password are provided but file doesn't exist": 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.keystorePath = some("/inexistent/file")
manager.keystorePassword = some("password") manager.keystorePassword = some("password")
@ -124,25 +123,17 @@ suite "Onchain group manager":
raiseAssert "Expected error when keystore file doesn't exist" raiseAssert "Expected error when keystore file doesn't exist"
asyncTest "startGroupSync: should start group sync": asyncTest "startGroupSync: should start group sync":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
(await manager.startGroupSync()).isOkOr: (await manager.startGroupSync()).isOkOr:
raiseAssert $error raiseAssert $error
await manager.stop()
asyncTest "startGroupSync: should guard against uninitialized state": asyncTest "startGroupSync: should guard against uninitialized state":
let manager = await setup()
(await manager.startGroupSync()).isErrOr: (await manager.startGroupSync()).isErrOr:
raiseAssert "Expected error when not initialized" raiseAssert "Expected error when not initialized"
await manager.stop()
asyncTest "startGroupSync: should sync to the state of the group": asyncTest "startGroupSync: should sync to the state of the group":
let manager = await setup()
let credentials = generateCredentials(manager.rlnInstance) let credentials = generateCredentials(manager.rlnInstance)
let rateCommitment = getRateCommitment(credentials, UserMessageLimit(1)).valueOr: let rateCommitment = getRateCommitment(credentials, UserMessageLimit(1)).valueOr:
raiseAssert $error raiseAssert $error
@ -182,10 +173,8 @@ suite "Onchain group manager":
check: check:
metadataOpt.get().validRoots == manager.validRoots.toSeq() metadataOpt.get().validRoots == manager.validRoots.toSeq()
merkleRootBefore != merkleRootAfter merkleRootBefore != merkleRootAfter
await manager.stop()
asyncTest "startGroupSync: should fetch history correctly": asyncTest "startGroupSync: should fetch history correctly":
let manager = await setup()
const credentialCount = 6 const credentialCount = 6
let credentials = generateCredentials(manager.rlnInstance, credentialCount) let credentials = generateCredentials(manager.rlnInstance, credentialCount)
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
@ -231,10 +220,8 @@ suite "Onchain group manager":
check: check:
merkleRootBefore != merkleRootAfter merkleRootBefore != merkleRootAfter
manager.validRootBuffer.len() == credentialCount - AcceptableRootWindowSize manager.validRootBuffer.len() == credentialCount - AcceptableRootWindowSize
await manager.stop()
asyncTest "register: should guard against uninitialized state": asyncTest "register: should guard against uninitialized state":
let manager = await setup()
let dummyCommitment = default(IDCommitment) let dummyCommitment = default(IDCommitment)
try: try:
@ -248,10 +235,7 @@ suite "Onchain group manager":
except Exception: except Exception:
assert false, "exception raised: " & getCurrentExceptionMsg() assert false, "exception raised: " & getCurrentExceptionMsg()
await manager.stop()
asyncTest "register: should register successfully": asyncTest "register: should register successfully":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
(await manager.startGroupSync()).isOkOr: (await manager.startGroupSync()).isOkOr:
@ -276,11 +260,8 @@ suite "Onchain group manager":
check: check:
merkleRootAfter.inHex() != merkleRootBefore.inHex() merkleRootAfter.inHex() != merkleRootBefore.inHex()
manager.latestIndex == 1 manager.latestIndex == 1
await manager.stop()
asyncTest "register: callback is called": asyncTest "register: callback is called":
let manager = await setup()
let idCredentials = generateCredentials(manager.rlnInstance) let idCredentials = generateCredentials(manager.rlnInstance)
let idCommitment = idCredentials.idCommitment let idCommitment = idCredentials.idCommitment
@ -310,10 +291,7 @@ suite "Onchain group manager":
await fut await fut
await manager.stop()
asyncTest "withdraw: should guard against uninitialized state": asyncTest "withdraw: should guard against uninitialized state":
let manager = await setup()
let idSecretHash = generateCredentials(manager.rlnInstance).idSecretHash let idSecretHash = generateCredentials(manager.rlnInstance).idSecretHash
try: try:
@ -323,10 +301,7 @@ suite "Onchain group manager":
except Exception: except Exception:
assert false, "exception raised: " & getCurrentExceptionMsg() assert false, "exception raised: " & getCurrentExceptionMsg()
await manager.stop()
asyncTest "validateRoot: should validate good root": asyncTest "validateRoot: should validate good root":
let manager = await setup()
let credentials = generateCredentials(manager.rlnInstance) let credentials = generateCredentials(manager.rlnInstance)
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
@ -372,10 +347,8 @@ suite "Onchain group manager":
check: check:
validated validated
await manager.stop()
asyncTest "validateRoot: should reject bad root": asyncTest "validateRoot: should reject bad root":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
(await manager.startGroupSync()).isOkOr: (await manager.startGroupSync()).isOkOr:
@ -405,10 +378,8 @@ suite "Onchain group manager":
check: check:
validated == false validated == false
await manager.stop()
asyncTest "verifyProof: should verify valid proof": asyncTest "verifyProof: should verify valid proof":
let manager = await setup()
let credentials = generateCredentials(manager.rlnInstance) let credentials = generateCredentials(manager.rlnInstance)
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
@ -451,10 +422,8 @@ suite "Onchain group manager":
check: check:
verified verified
await manager.stop()
asyncTest "verifyProof: should reject invalid proof": asyncTest "verifyProof: should reject invalid proof":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
(await manager.startGroupSync()).isOkOr: (await manager.startGroupSync()).isOkOr:
@ -500,10 +469,8 @@ suite "Onchain group manager":
check: check:
verified == false verified == false
await manager.stop()
asyncTest "backfillRootQueue: should backfill roots in event of chain reorg": asyncTest "backfillRootQueue: should backfill roots in event of chain reorg":
let manager = await setup()
const credentialCount = 6 const credentialCount = 6
let credentials = generateCredentials(manager.rlnInstance, credentialCount) let credentials = generateCredentials(manager.rlnInstance, credentialCount)
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
@ -557,10 +524,8 @@ suite "Onchain group manager":
manager.validRoots.len() == credentialCount - 1 manager.validRoots.len() == credentialCount - 1
manager.validRootBuffer.len() == 0 manager.validRootBuffer.len() == 0
manager.validRoots[credentialCount - 2] == expectedLastRoot manager.validRoots[credentialCount - 2] == expectedLastRoot
await manager.stop()
asyncTest "isReady should return false if ethRpc is none": asyncTest "isReady should return false if ethRpc is none":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
@ -575,10 +540,7 @@ suite "Onchain group manager":
check: check:
isReady == false isReady == false
await manager.stop()
asyncTest "isReady should return false if lastSeenBlockHead > lastProcessed": asyncTest "isReady should return false if lastSeenBlockHead > lastProcessed":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
@ -591,10 +553,7 @@ suite "Onchain group manager":
check: check:
isReady == false isReady == false
await manager.stop()
asyncTest "isReady should return true if ethRpc is ready": asyncTest "isReady should return true if ethRpc is ready":
let manager = await setup()
(await manager.init()).isOkOr: (await manager.init()).isOkOr:
raiseAssert $error raiseAssert $error
# node can only be ready after group sync is done # node can only be ready after group sync is done
@ -610,8 +569,6 @@ suite "Onchain group manager":
check: check:
isReady == true isReady == true
await manager.stop()
################################ ################################
## Terminating/removing Anvil ## Terminating/removing Anvil
################################ ################################

View File

@ -29,6 +29,3 @@ proc deployContract*(
let r = await web3.send(tr) let r = await web3.send(tr)
return await web3.getMinedTransactionReceipt(r) return await web3.getMinedTransactionReceipt(r)
proc ethToWei*(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256

View File

@ -12,7 +12,8 @@ import
web3, web3,
json, json,
libp2p/crypto/crypto, libp2p/crypto/crypto,
eth/keys eth/keys,
results
import import
waku/[ waku/[
@ -101,28 +102,43 @@ proc uploadRLNContract*(ethClientAddress: string): Future[Address] {.async.} =
return proxyAddress return proxyAddress
proc createEthAccount*( proc sendEthTransfer*(
ethAmount: UInt256 = 1000.u256 web3: Web3,
): Future[(keys.PrivateKey, Address)] {.async.} = accountFrom: Address,
let web3 = await newWeb3(EthClient) accountTo: Address,
let accounts = await web3.provider.eth_accounts() amountWei: UInt256,
let gasPrice = int(await web3.provider.eth_gasPrice()) accountToBalanceBeforeExpectedWei: Option[UInt256] = none(UInt256),
web3.defaultAccount = accounts[0] ): Future[TxHash] {.async.} =
let doBalanceAssert = accountToBalanceBeforeExpectedWei.isSome()
let pk = keys.PrivateKey.random(rng[]) if doBalanceAssert:
let acc = Address(toCanonicalAddress(pk.toPublicKey())) 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 var tx: EthSend
tx.source = accounts[0] tx.source = accountFrom
tx.value = some(ethToWei(ethAmount)) tx.to = some(accountTo)
tx.to = some(acc) tx.value = some(amountWei)
tx.gasPrice = some(gasPrice) tx.gasPrice = some(gasPrice)
# Send ethAmount to acc # TODO: handle the error if sending fails
discard await web3.send(tx) let txHash = await web3.send(tx)
let balance = await web3.provider.eth_getBalance(acc, "latest")
assert balance == ethToWei(ethAmount), if doBalanceAssert:
fmt"Balance is {balance} but expected {ethToWei(ethAmount)}" 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) return (pk, acc)
@ -189,8 +205,11 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
except: except:
error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg() error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg()
proc setup*( proc ethToWei(eth: UInt256): UInt256 =
ethClientAddress: string = EthClient, ethAmount: UInt256 = 10.u256 eth * 1000000000000000000.u256
proc setupOnchainGroupManager*(
ethClientAddress: string = EthClient, amountEth: UInt256 = 10.u256
): Future[OnchainGroupManager] {.async.} = ): Future[OnchainGroupManager] {.async.} =
let rlnInstanceRes = let rlnInstanceRes =
createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain")) createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain"))
@ -206,15 +225,19 @@ proc setup*(
let accounts = await web3.provider.eth_accounts() let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[0] web3.defaultAccount = accounts[0]
var pk = none(string) let (privateKey, acc) = createEthAccount(web3)
let (privateKey, _) = await createEthAccount(ethAmount)
pk = some($privateKey) # 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( let manager = OnchainGroupManager(
ethClientUrl: ethClientAddress, ethClientUrl: ethClientAddress,
ethContractAddress: $contractAddress, ethContractAddress: $contractAddress,
chainId: CHAIN_ID, chainId: CHAIN_ID,
ethPrivateKey: pk, ethPrivateKey: some($privateKey),
rlnInstance: rlnInstance, rlnInstance: rlnInstance,
onFatalErrorAction: proc(errStr: string) = onFatalErrorAction: proc(errStr: string) =
raiseAssert errStr raiseAssert errStr

View File

@ -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"))

View File

@ -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()

View File

@ -1,5 +1,4 @@
import json_serialization, std/options import std/options
import ../waku_core
# Implementing the RFC: # Implementing the RFC:
# https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/73 # https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/73