feat(evm): Besu test network and the fungible suite running on it - #2159
Open
atharrva01 wants to merge 27 commits into
Open
feat(evm): Besu test network and the fungible suite running on it#2159atharrva01 wants to merge 27 commits into
atharrva01 wants to merge 27 commits into
Conversation
…r wiring Completes the driver so it can be built from configuration alone. A recipient only ever holds the token request anchor, so it needs a way to find the transaction that applied it. That comes from the TokenState's commit event, filtered on the indexed anchor: the transaction hash is not in the payload and cannot be, since a contract has no way to read its own hash, so it is taken from the log record the node returns. The cheaper getTokenRequestHash call stays the common check, because log queries need a block range and retention varies between nodes. Local membership is injected rather than invented, since the driver does not own identities, and the submitter is now built from the configured key, with the derived address checked against the configured one so a mismatched pair fails at startup instead of producing signatures nobody accepts. Also splits the EIP-712 type string across lines for the newly enabled line length linter. The concatenation is compile time and the golden digests are unchanged. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Adds evmdlog, the node SDK for a TMS backed by an EVM network, mirroring fdlog and fxdlog. It registers the EVM driver in the same network-drivers group the fabric drivers use, so the network provider picks whichever one recognises a network from its configuration. It lives in the integration module rather than the driver's own module because wiring needs token/sdk/dig, which pulls in core's fabric and idemix graph. The driver is a separate module kept lean enough to live in its own repository, and that graph cannot be version reconciled inside it. The dependency runs one way: this imports the driver, never the reverse. The wiring test resolves the whole graph, so a driver the container cannot build fails here rather than at node start with a network configured. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
The seam where the test network and the driver have to agree: NWO generates a node's token configuration, and the driver has to accept it. The fabric extension turns out to be almost entirely backend agnostic, since the TMS coordinates, certification, storage and wallets are token-level concerns, so this mirrors it and swaps only the network block for the evm schema. The round trip test renders the template and loads the result through the driver's own LoadConfig rather than comparing against a hand written expectation, which would only prove the template matches itself. It covers a node that endorses and broadcasts, one that does neither but still needs the endorser set to route requests, and the finality defaults. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Starts a real Besu in a container and waits until it answers JSON-RPC, so a caller gets a node that is actually usable rather than one whose container merely started. It follows the container lifecycle the fabricx extension uses. Besu rather than a lighter development chain because it is the acceptance backend: the point of the integration suite is that the driver works against the real node. It runs in development mode, which mines instantly and pre-funds accounts, so only consensus and funding are shortcut, not the client behaviour the driver exercises. Verified against a real container: the node comes up in about five seconds and the driver's own client reads the chain id back from it. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
…ities Stands a TMS up on a running node using the project's own forge script, so the test network deploys the way an operator would rather than through a parallel path that could drift. Endorser identities are generated fresh per network, since an endorser only ever signs digests off chain and never sends a transaction, so its key needs no funding. The submitter is a pre-funded development account, because it does pay for gas. Also compiles the contracts below Shanghai. Besu's development network does not enable EIP-3855, so a PUSH0 in the deployment bytecode made every contract creation revert on chain, which is how this surfaced: the transactions were mined and failed. Nothing here needs a post-Paris opcode, and the 47 contract tests and the cross-implementation digests are unchanged. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Assembles the pieces into the handler the token platform drives, and registers evm as a selectable backend alongside fabric and fabricx. Generating artifacts brings the chain up, provisions endorser identities and deploys the contracts, all before any node configuration is rendered. That ordering is forced: a node's configuration has to name the contract addresses, and those do not exist until the deployment has run. PostRun consequently has nothing left to do. Issuer and owner wallets are generated the same way every backend generates them, since those identities belong to the token layer rather than the chain, so only the genuinely EVM specific parts are supplied here. Updating public parameters is refused: after bootstrap they belong to the endorser quorum and an update travels as an endorsed setup delta, not as a test network action. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Closes the two gaps that stopped a node from building a TMS on an EVM network, both found by running the suite rather than by reading the code. The token drivers read LocalMembership().DefaultIdentity() while constructing a TMS, and the driver returned nil, so a node panicked at startup. Identities now come from the FSC identity provider, which owns them. Anonymous identities return the default one: EVM has no network-layer anonymity to draw on, and token privacy comes from the owner wallets regardless. Normalize also has to install the public parameters fetcher. The token layer looks for parameters in the options, its storage, the local configuration and finally that fetcher, so without it a fresh node cannot find them at all and the failure surfaces far from its cause. Adds the per-TMS endorsement service factory. The service cannot be built per network because validating a request needs that TMS's validator; it takes the TMS from the approval path rather than reaching for a provider, since a TMS is built through the network driver and reaching back would close a cycle. Public parameters for a delta are read from the contract, so the hash and version an endorser signs are consistent with what the contract checks at apply time. Also registers the suite: the evm case in the topology switch, a platform for the infrastructure to resolve, the fungible wrapper calling the shared test bodies unchanged, and a make target that pulls the besu image. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
An endorser answers requests without ever making one, so registering its responder lazily on the approval path meant it never registered at all and every request to it timed out. Registration now happens when the network is built. That in turn means the responder cannot be tied to a TMS at construction, since it has to exist before its TMS has necessarily been built, and building one goes through this driver. It now resolves the TMS from the id the request carries, and refuses a request for a TMS it cannot resolve, which is the same check the fixed identity used to make, expressed through what it can actually validate. Verified on the integration network: all three endorser nodes register. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
The endorsement configuration names nodes, but a session is opened to an identity and the allowlist is compared against the identity a session authenticated. Building both straight from the configured strings meant the initiator opened sessions to nothing and the endorser could never match a caller, so every request timed out. Resolve the names through the FSC identity provider, the way the fabric endorsement service already does. An endorser that cannot be resolved is fatal, since the quorum could never be met; an allowlist entry that cannot be resolved is dropped, since it could never match anyway and a node does not resolve its own name. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
… the node has no opinion Two things stood between the test network and a working issue. The token configuration listed the whole network's wallets on every node instead of the wallets that node was issued, which left each of them without a usable identity of its own. The per-node wallets are tracked by the materials handler, so take them from there. Besu does not implement eth_maxPriorityFeePerGas. It is a de-facto extension rather than part of the JSON-RPC spec, so a node is entitled not to, and the submitter treated its absence as fatal. Fall back to the part of eth_gasPrice that sits above the base fee, which is the same quantity computed from a method every node implements. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
…ough an endorsed setup delta The topology conflated the network's name with the technology backing it, so an EVM network came up as "evm" while the suites address networks as "default", the name the fabric and fabricx topologies use. Type still says evm, which is what the token platform routes on. The TokenState contract has no administrative setter for public parameters: the only way they change is an endorsed setup delta. The test network holds every endorser key, so it can produce the quorum the contract asks for, which is what the operator of a real network would have to do too. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
…token too Public parameters change through an endorsed setup delta that some other node submits, so unlike a transaction this node sent there is nothing local to trigger off, and a node kept serving whatever it started with. Watch the version counter the TokenState contract keeps and reload the TMS when it moves, which is the same job fabric's listener on the setup key does. It polls the version rather than subscribing to the log. The version is one cheap call against current state, so there is no from-block to carry across restarts and nothing to reconcile after a reorg. Also adds an evmfabtoken SDK and the matching suite, so the same shared test bodies run over the EVM driver with both token drivers. That is worth the duplication: the two put very different work through one network driver, so a failure under one and not the other says where the problem is not. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
The unaligned keys in the FuncMap literal fail 'gofmt -l -s', which fails make checks, which gates both utest and make lint in the same job. Nothing on the branch had been linted or unit tested by CI because of it. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
A node learns that a transaction it holds became final through a finality listener the ttx layer registers when it stores that transaction. That registration lives in memory. A node that restarts in between is left with a row stuck at Pending and nothing that will ever move it: the chain has the answer and nobody is asking, so every later wait on that transaction runs to its timeout. Start the SDK's recovery manager over the transaction store and the audit store, per TMS, from Connect. The Fabric driver starts exactly these two for the same reason and fabricx inherits them; this driver started none, which stays invisible for as long as no node restarts. Failing to start it is logged rather than returned. The node works without recovery, and refusing the connection would take it out over a facility it needs only for transactions it may not have. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Estimating gas executes the transaction, so an estimate that reverts is the chain rejecting the delta: a double spend, stale public parameters, a quorum the contract will not accept. Sending it anyway mines it with status 0 and reaches the same verdict, having paid for it. Every other failure of the same call says nothing about the transaction and has to be retried instead. Both came back as one wrapped error, so a caller could only tell them apart by matching strings. Classify at the client, where the JSON-RPC code and message are still in hand: the code alone is not enough, since -32000 is the implementation-defined range and covers every server-side execution failure, and the message alone is not enough, since clients word it differently. Pair the two. The driver then wraps a revert as ErrTransactionReverted and the rest as ErrNetworkUnavailable, which is the permanent/transient split callers act on. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Two instances of one mistake: state that a parameters update replaces, captured by something that outlives the update. ChainProvider read the parameter bytes fresh but took the version from a keeper only the watcher invalidated. The pair travels together in a delta and the contract checks both, so after an update every delta carried the new bytes under the old version and was rejected with StalePublicParams, for as long as the cache lived. It surfaced only as eth_estimateGas reverting, with nothing pointing at parameters. Both halves are read per call now, at one extra eth_call. The initiator captured a *token.ManagementService, which Update evicts so the next caller builds a new one. The captured pointer kept validating against superseded parameters, so after an update authorising a new issuer that issuer's every request was rejected as unauthorized, by a node that had already logged the new parameters. It holds a TMS id and resolves per request now, as the responder always did. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
The harness handed every node the same pre-funded development account. Nonces are per account and each node's NonceManager counts locally, so the second node to broadcast reused a consumed nonce and got nonce too low. The harness's own parameters update, spending from that same account, moved the nodes' nonces behind their backs in the same way. Generate and fund an account per node, and keep a separate operator account for deploying and for submitting the setup delta: that is the harness acting as the network's operator rather than as any node, and spending from a node's account would move that node's nonce without telling it. Funding has to run after the chain is up, which is why it is not next to the endorser identities. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Deploying in two transactions leaves a window between the clone and its initialize in which anyone can initialize it first, and the deployer then records an address whose endorser set belongs to someone else. TokenStateFactory closes it by doing both in one call. Deploy.s.sol also reads the post-initialize state back before recording the address, so a deploy that somehow lands wrong is caught at deploy time rather than at the first transaction. The test suite pins both halves, including running the hijack against the two-transaction deploy this replaces, so the window it closes is demonstrated rather than asserted. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
The runbook enumerates the bootstrap an operator performs: deploy the verifier and the TokenState clone, seed public parameters, register the endorser set, threshold and graphHiding. It is the spec the forge and NWO scripts automate. The design doc catches up with what the integration run changed, per the rule that deviations land as design edits rather than as commit messages: nothing cached across a parameters update, transaction recovery, Besu's JSON-RPC gaps and the paris compile target, and which of the error taxonomy is built. network-ethereum.md gains an operational section, since error classes, recovery and one funded account per submitting node are things an operator has to know before running this and could previously only learn by reading the driver. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
…ries The goimports failure on topology.go is what CI was red on; it short-circuited make checks before staticcheck ever ran, which was hiding two more. weekly sync.html was never meant to be committed. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
A revert writes nothing to the chain, so an anchor that never appears is all a rejection ever looks like. The design says an unseen anchor is Unknown, then Invalid after the configured timeout. That escalation existed in AddListener, which knows when it started waiting, and not in StatusByAnchor, which is what GetTransactionStatus and therefore recovery call. The shared recovery handler reads Unknown as transient and looks again next sweep, so a rejected transaction was claimed every five seconds forever, each sweep logging success and changing nothing. Its record stayed Pending and the holding it had reserved was never released, which is what the suite saw: the right balance next to a stale holding. Recovery now resolves a still-absent anchor to Invalid. What makes that safe is when it is asked: the manager only claims rows older than the TTL, and the TTL is raised to the finality timeout, so the database has already established that the transaction outlived the window in which one is ever awaited. That clock is a column rather than a field, so it survives the restart recovery exists for. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
The previous commit licensed recovery's invalid verdict by raising the recovery TTL to the finality timeout, so the manager only claimed rows old enough to condemn. That also delayed the committed case, which is the one where the chain had the answer all along: a recipient whose transfer had landed was no longer recovered within the thirty seconds a finality check allows, and the suite failed an assertion earlier than before. The TTL answers how soon it is worth asking again. The finality timeout answers how long absence means rejection. Reading the transaction row's own timestamp keeps the sweep frequent and the verdict patient, and it is still a column rather than a field, so it survives the restart recovery exists for. A store that cannot be read leaves the transaction in the sweep rather than condemning it, since that is the one irreversible move available here. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
…is what it is The fabtoken suite runs green end to end on Besu with the shared bodies unmodified, including the concurrent transfers and the parallel token selector that no earlier run had reached. The timeout comment is the part worth keeping. Two minutes looks absurd next to a chain that mines instantly, and shortening it to match the chain deletes transfers: a transaction that has been prepared but not yet broadcast has no anchor on chain, which is the same evidence a rejected one leaves, so recovery condemns it. The suite prepares two transfers, restarts two nodes, and only then broadcasts. The binding lower bound is not the chain's finality, it is the longest gap the application leaves between assembling a transaction and sending it. The design already said an absent anchor is indistinguishable from one that was never submitted. It said so in the section that then treated absence past a deadline as proof of rejection. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
ginkgo -cover writes coverprofile.out next to the suite it ran, and it was committed. It is a build artifact of whoever ran the suite last. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
testifylint wants require for error assertions, and these three are the ones that say which class a failure belongs to. Continuing past a wrong class only produces a second, more confusing failure on the next line. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
…e design doc on deployment AGENTS.md asks for a fuzz target on every exported function that parses untrusted input, and the evm module had none while the rest of the repo has twenty six. The driver is mostly parsers: endorsement signatures from remote peers, envelopes off the wire, ABI responses from a node, hex from configuration. Nine targets over the entry points that take bytes somebody else chose. They check more than the absence of a panic, since that alone would pass on a decoder that quietly truncates: a recovered signature has to satisfy the same format rules the verifier contract enforces, a decoder must not return more than it was given, a parsed address has to round trip, and moving a byte across the nonce and creator boundary has to change the anchor. Wiring them in needed a working directory in the nightly matrix, since the evm module is its own and go test cannot reach it from the repository root. The crash collection step needed the same, or a failing evm target would have uploaded no reproducer. Design §3.8 still described the deploy-hardening factory as future work. It exists, the deploy script goes through it, and the post-initialize check that paragraph called for now guards against duller failures than hijacking. Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stands up an EVM-backed test network and runs the existing fungible suite against it, with the shared test bodies unchanged.
The network handler boots a Besu node in a container, deploys the contracts through the project's own forge script, generates an endorser identity per endorsing node plus a funded submitter, and writes each node a token configuration pointing at what it just deployed. The rendered config is parsed back by the driver's own
LoadConfigin a test, so the harness and the driver cannot drift apart.On the driver side this fills in what the suite needed and was missing:
A few things only showed up once real nodes were talking to each other:
eth_maxPriorityFeePerGas. It is an extension rather than part of the JSON-RPC spec, so the submitter falls back to the part ofeth_gasPriceabove the base fee.Public parameters are updated through an endorsed setup delta, since the TokenState contract has no administrative setter. A factory creates and seeds a clone in one transaction, so there is no window between the two for anyone else to initialize it.
There are two suites, zkatdlog and fabtoken, over the same driver. That is worth the duplication: the two token layers put very different work through one network driver, so a failure under one and not the other says where the problem is not.
make integration-tests-evmandmake integration-tests-evm-fabtokenrun them. They need docker and forge, and pull the besu image if missing. No FAB_BINS.The fabtoken suite passes end to end on a real Besu node with
fungible.TestAllunmodified. The zkatdlog one still stops on a time-window assertion inCheckAuditedTransactions; details are in a comment below, and it is being tracked separately rather than held against this branch.There is also a deployment runbook under
docs/services/network-ethereum-deployment.md.