mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-01-02 14:03:06 +00:00
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:
parent
dd1a70bdb7
commit
fdfc48c923
@ -1 +1 @@
|
||||
import ./test_rpc_codec
|
||||
import ./test_rpc_codec, ./test_poc
|
||||
|
||||
198
tests/incentivization/test_poc.nim
Normal file
198
tests/incentivization/test_poc.nim
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
################################
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
9
waku/incentivization/common.nim
Normal file
9
waku/incentivization/common.nim
Normal 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"))
|
||||
91
waku/incentivization/eligibility_manager.nim
Normal file
91
waku/incentivization/eligibility_manager.nim
Normal 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()
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user