docs(blockchain): make every Operation carry a proof and gas independent of it - #431
Open
thomaslavaur wants to merge 1 commit into
Open
docs(blockchain): make every Operation carry a proof and gas independent of it#431thomaslavaur wants to merge 1 commit into
thomaslavaur wants to merge 1 commit into
Conversation
7 tasks
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.
Audit issue #136 reports that one block id can carry two bodies and produce two ledger states, because gas is billed off proof bytes that nothing in the chain of commitments covers. The free slot is a
CHANNEL_CONFIGon a channel that does not exist yet, whose proof is never inspected on any path. This PR closes the class from the other side: every Operation carries exactly one proof, the genesis configuration proof is verified against a threshold of0instead of being ignored, and the Execution Gas of an Operation is derived from the Operation and the state it is validated against rather than from the proof accompanying it.Reviewer Orientation
Read Motivation first, then the audit issue, whose failure sequence is what the three normative changes below are answering. The review question is whether the number and the shape of every proof is now a function of the Operations and the state alone, so that two bodies sharing a
mantle_txhashcan no longer be priced differently.elsebranch now verifies instead of falling through, and that a threshold of0accepts exactly one proof, the empty one0case for a channel that does not exist yetNonecaseStatus tracker
Change log
Motivation
The
mantle_txhashcovers the Operations of a Mantle Transaction and nothing else, and the block body root is built from that hash, so the proofs a transaction carries are outside every commitment a block header makes. That would be harmless if the proofs were only read by verification, which either accepts or rejects. They are not: the mandatory fee reads them twice, once through the encoded size of the signed Mantle Transaction and once through the thresholds pricing the channel Operations. A quantity that no commitment pins is therefore an input of the ledger state a block produces.Audit issue #136 shows what that costs. A
CHANNEL_CONFIGsent to a channel that does not exist yet is authorized by nobody, since there is no accredited key to authorize it, and the specification said so by leaving the proof unmentioned in that branch. An implementation reading it built a validation path where the proof of a channel creating configuration is never inspected, so a proposer could win a slot, build one header and two bodies whose proofs differ only in how many signatures they carry, and send one to each half of the network. Both halves recompute the samebody_rootand accept the same header, and then disagree on the gas the block consumed. Sized loudly the fat body fails its balance check and the network partitions on the first message, sized quietly the two halves diverge on fee market state with nothing surfacing the disagreement.The specification can remove the degree of freedom the attack needs, which is that two well formed proof lists exist for the same Operations under the same state. Once the variant of every proof is fixed by its Operation and its number of signatures is fixed by the state, the proof list has one shape, one encoded size and one gas cost. The failure then requires forging a signature rather than choosing how many to send, and the same reasoning covers every Operation added later, since a new Operation gets its proof shape from the same two inputs.
Proposal
Three changes, in Mantle, and their consequence in Mantle Transaction Encoding.
op_proofsloses itsNonecase. Every Operation carries exactly one proof, of the variant that Operation requires. None of the ten opcodes was using theNonecase, each of them already defining a proof in its ownProofsection, so this removes a possibility rather than a behavior.A
CHANNEL_CONFIGcreating a channel is verified rather than skipped. The branch that previously only checked theparentnow runs the same Multiple Ed25519 Signatures Verification as the branch for an existing channel, against an empty accredited key list and a threshold of0. That routine asserts that the number of signatures equals the threshold, so a threshold of0accepts exactly one proof, the one carrying an empty signature list and an empty index list, encoded as a count of0and nothing else.The Execution Gas of an Operation is stated to be a function of the Operation and the state it is validated against. Where an Operation is priced proportionally to a threshold, that threshold is the one held in the channel state, and it is
0for a channel that does not exist yet. The proof is not an input.The encoding follows: the
OpProofalternatives gainChannelConfigOpProofandChannelTransferOpProof, which were specified in Mantle but had no production, and the three channel proofs share aChannelMultiSigProofproduction that carries the index of the signing key alongside each signature.Discussion
Why not commit the proofs in the transaction hash
The audit's primary recommendation is to include
op_proofsin the transaction identity hash. It cannot be done at themantle_txhash, which every proof is bound to as a public input: hashing the proofs into the value they sign is circular. It could be done one level up, by making the body root leaf a hash over the full encoded signed Mantle Transaction, which is the audit's second formulation. That is a change to Block Construction rather than to Mantle, it changes what a block id is, and it is worth considering on its own merits.This PR does not depend on that decision. Committing to the proof bytes would make the two bodies distinguishable, so the substitution would produce two block ids instead of one. Removing the freedom makes the second body impossible to build in the first place, whichever identity a block has. The two are complementary, and only the second one keeps holding for Operations that do not exist yet.
What this does and does not pin
The
mantle_txhashstill does not determine the proof bytes, and this PR does not claim it does. What it determines, together with the state, is the shape ofop_proofs: how many entries it has, which variant each entry is, and how many signatures each entry carries. Every proof variant has a size fixed by that number, so the encoded size of the signed Mantle Transaction, and with it the permanent storage gas, is a function of the Operations and the state. The Execution Gas is one by construction after this PR. Two bodies sharing amantle_txhashtherefore consume the same gas, which is the property the divergence needed.Substituting signature bytes for other valid signature bytes remains possible for whoever can produce them, which is the holder of the accredited key. It changes no size, no threshold and no gas, so it leads to the same ledger state.
The cost of a channel creating configuration
Pricing a
CHANNEL_CONFIGatEXECUTION_CHANNEL_CONFIG_GAS * configuration_thresholdwith a threshold of0makes a channel creating configuration cost no Execution Gas. This is the honest answer of the linear formula, which prices the signatures verified, and a channel creating configuration verifies none. The Operation is not free: it pays permanent storage gas proportional to its encoded size, which for a configuration carrying a list of accredited keys is the dominant component, and it pays it in a Mantle Transaction whose balance must cover the mandatory fee like any other. A reviewer who considers a floor preferable should say so, the alternative being an affine cost with a base term.Backwards compatibility
The removal of the
Nonecase is not a wire change, no encoding ever carried a discriminant for it. TheChannelMultiSigProofproduction is a correction of the specification rather than a change of the encoding: the previousChannelWithdrawOpProofproduction omitted the signer indexes that theChannelWithdrawOpProofstructure of Mantle has always carried, so a decoder written from it could not have interoperated. The order is the one the implementation uses, verified againstcalculate_channel_multi_sig_proof_byte_sizeincore/src/proofs/channel_multi_sig_proof.rs, which is2 + N * (ED25519_SIGNATURE_SIZE + 2).The one behavioral change for a conforming implementation is the genesis configuration proof. A
CHANNEL_CONFIGcreating a channel with a non-empty proof was accepted before and is invalid now, and its Execution Gas drops fromEXECUTION_CHANNEL_CONFIG_GAStimes the number of signatures it happened to carry to0. Both are consensus affecting and must land together.Details
1. Every Operation carries a proof
Mantle drops the
Nonealternative from the signed transaction:class SignedMantleTx: tx: MantleTx - op_proofs: list[OpProof | None] # each Op has at most 1 associated proof + op_proofs: list[OpProof] # each Op has exactly 1 associated proofand states in the first validation step what the list must hold, the length assertion being unchanged:
Each of the ten opcodes already specifies its proof in its own
Proofsection, so no Operation loses a way of being expressed. Genesis Block already required one entry per Operation of the type that Operation requires, its entries being placeholders that are not verified, and needs no change.2. The genesis configuration proof
The
CHANNEL_CONFIGvalidation branch for a channel that does not exist yet checked theparentand returned. It now verifies the proof against the threshold that a channel with no accredited key implies:else: # Channel will be created automatically upon execution # Ensure that this configuration is the genesis configuration assert config.parent == ZERO + + # No key is accredited yet, so the threshold to verify against is 0 + # and the proof must carry no signature and no index (see Appendix) + MultiEd25519_verify(txhash, + proof.signatures, + proof.indexes, + [], + 0)The routine is unchanged. Its first two assertions are what a threshold of
0needs, one index per signature and exactlythresholdsignatures, so both lists must be empty, the ordering loop is vacuous and the verification loop is empty:The accredited key list passed is empty, which is the state of a channel before its creation, and no index can reach it since there is no index.
3. Execution Gas as a function of the Operation and the state
The fee computation takes the state the Operation is validated against, and passes it to the gas function:
followed by the statement the reader needs when specifying a new Operation:
+The Execution Gas of an Operation is deterministically derived from that Operation and the state it is validated against.The three Operations priced proportionally to a threshold now name where that threshold is read, which is the channel state and not the proof:
For
CHANNEL_CONFIGthis also resolves which of the two thresholds was meant. It is the one the channel holds, which is the number of signatures the Operation makes a validator verify, and not the one the payload proposes, which costs nothing to install.CHANNEL_WITHDRAWandCHANNEL_TRANSFERboth require the channel to exist, so their threshold is always defined.4. The channel threshold proof productions
Mantle Transaction Encoding listed one channel threshold proof out of three and encoded it without the signer indexes:
OpProof = Ed25519SigProof / ZkSigProof / ZkAndEd25519SigsProof / + ChannelConfigOpProof / ChannelWithdrawOpProof / + ChannelTransferOpProof / ProofOfClaimProof Ed25519SigProof = Ed25519Signature ZkSigProof = ZkSignature ZkAndEd25519SigsProof = ZkSignature Ed25519Signature -ChannelWithdrawOpProof = SignatureCount *Ed25519Signature +ChannelConfigOpProof = ChannelMultiSigProof +ChannelWithdrawOpProof = ChannelMultiSigProof +ChannelTransferOpProof = ChannelMultiSigProof ProofOfClaimProof = Groth16 +ChannelMultiSigProof = SignatureCount *IndexedSignature +IndexedSignature = Ed25519Signature SignerIndex + SignatureCount = UINT16 +SignerIndex = UINT16The three proofs are the same structure in Mantle, a list of signatures with the index of the signing key for each, so they share one production. The signature precedes its index within an entry, and the entries are prefixed by their count, which makes a proof
2 + N * 66bytes and an empty proof the two bytes encoding a count of0.Chores
CHANNEL_CONFIGexample of Mantle, whoseop_proofsdid not nest the signature list and the index list into a single proof.transfer_thresholdthat Mantle holds rather than thestake_manipulation_thresholdit was called before the channel Proof of Stake update, and the Channel Stake Assignation section being the Channel Transfer one.1.12.0in Mantle,1.7.0in Mantle Transaction Encoding and1.5.3in Gas Cost Determination.Implementation
Nonecase from the operation proof list, every Operation carrying exactly one proof of the variant its opcode requiresCHANNEL_CONFIGon a channel that does not exist yet whose proof carries any signature or any index, by running the multi signature verification against an empty key list and a threshold of0instead of returning without inspecting the proofCHANNEL_CONFIG,CHANNEL_WITHDRAWandCHANNEL_TRANSFERfrom the threshold held in the channel state,0for a channel that does not exist yet, and never from the length of the signature list the proof carriesAffected Specifications
1.12.0, the proof list, theCHANNEL_CONFIGvalidation and the Execution Gas of the three channel Operations1.7.0, the two missingOpProofvariants and theChannelMultiSigProofproduction1.5.3, terminology only, the gas derivations naming astake_manipulation_thresholdthat no longer existsop_proofsentry per Operation of the type that Operation requires, consistent with the removal of theNonecaseVersion numbers to watch when merging: Mantle is at
1.11.1and Mantle Transaction Encoding at1.6.1onmaster, so both rows renumber if another PR lands first.