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

View File

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

View File

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

View File

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

View File

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

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 ../waku_core
import std/options
# Implementing the RFC:
# https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/73