Promote vFuture to a New Consensus Version
Turn the features staged in vFuture into a released, numbered consensus version
(the next natural number, vN+1), re-open vFuture as an empty shell with an
incremented LogicSigVersion, bump the AVM's LogicVersion so held-back
experimental opcodes have a home, and repair the tests that the shift exposes. This
mirrors past release commits (e.g. the v41 upgrade, PR #6422) and must leave
TestReleasedVersion passing.
This skill ships in the go-algorand repo and runs from its root.
The mental model
At rest, the code has a current top version vN (whatever
protocol.ConsensusCurrentVersion points at) and a vFuture built from vN that
layers on the not-yet-released parameters. Promotion "freezes" today's vFuture into
vN+1, wires the vN -> vN+1 upgrade path, and starts a new vFuture from vN+1.
Every promoted feature parameter moves out of the vFuture block and into the
new numbered block. The fresh vFuture block ends up with only one line of substance:
a LogicSigVersion one higher than the version just released.
Two things track vFuture.LogicSigVersion and must move with it:
- The AVM
LogicVersion(data/transactions/logic/opcodes.go) — the max version the assembler/evaluator supports. See Step 2b. - Nothing else automatically.
docVersion(the published-langspec ceiling) does not move — it stays at the released version. See Step 4.
Step 0: Discover the current state and get the URL
Find the current top version and vFuture block:
grep -n "ConsensusCurrentVersion = " protocol/consensus.go grep -n "vFuture :=\|ConsensusFuture\] = vFuture\|LogicSigVersion" config/consensus.goFrom
vFuture := vNandConsensusCurrentVersion = ConsensusVN, fix the numbers: the released version isvN, the new one isvN+1. Read the whole vFuture block inconfig/consensus.goand notevFuture.LogicSigVersion(call itL— the new version getsL, the new vFuture getsL+1) and everyvFuture.<Param> = ...line.Ask the user for the spec GitHub URL for the new version's constant string. Often the real URL is not ready. If so, use a placeholder that keeps the real spec-repo prefix but has an obviously-fake tail, e.g.:
const ConsensusV42 = ConsensusVersion( "https://github.com/algorandfoundation/specs/tree/TODO-REPLACE-BEFORE-RELEASE-v42", )The prefix is mandatory, not cosmetic:
config'sTestConsensusUpgrades(viacheckConsensusVersionName) rejects any version string in the upgrade path that does not start withhttps://github.com/algorandfoundation/specs/tree/(or the olderhttps://github.com/algorand/spec/tree/). A bareTODO-style string fails that test. Flag the placeholder loudly so it is replaced before release.
Step 1: protocol/consensus.go
- Add the new version constant immediately before
ConsensusFuture, doc comment summarizing the promoted features (paraphrase the vFuture params and the new TEAL version):// ConsensusV42 enables <summary of promoted features> and TEAL v<L>. const ConsensusV42 = ConsensusVersion( "<url-or-placeholder-with-real-prefix>", ) - Update the current-version pointer:
const ConsensusCurrentVersion = ConsensusV42.
Step 2a: config/consensus.go — the core transformation
Convert the existing vFuture := vN block into the new numbered block, then add a new
empty vFuture after it. The block that today reads:
// ConsensusFuture is used to test features that are implemented
// but not yet released in a production protocol version.
vFuture := v41
vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{}
vFuture.LogicSigVersion = 13 // When moving this to a release, put a new higher LogicSigVersion here
vFuture.AppSizeUpdates = true
vFuture.<...more feature params...>
Consensus[protocol.ConsensusFuture] = vFuture
becomes:
v42 := v41
v42.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{}
v42.LogicSigVersion = 13
v42.AppSizeUpdates = true
v42.<...more feature params...>
Consensus[protocol.ConsensusV42] = v42
// v41 can be upgraded to v42, with an update delay of 7d:
// 208000 = (7 * 24 * 60 * 60 / 2.9 ballpark round times)
// our current max is 250000
v41.ApprovedUpgrades[protocol.ConsensusV42] = 208000
// ConsensusFuture is used to test features that are implemented
// but not yet released in a production protocol version.
vFuture := v42
vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{}
vFuture.LogicSigVersion = 14 // When moving this to a release, put a new higher LogicSigVersion here
Consensus[protocol.ConsensusFuture] = vFuture
Checklist:
- Rename every
vFuture.in the old block tov42., keeping all feature params. - The new version's
LogicSigVersionisL; drop the "When moving this to a release..." trailing comment there. - Move the
// ConsensusFuture is used to test...comment to the new vFuture block. - Change
Consensus[protocol.ConsensusFuture] = vFuturetoConsensus[protocol.ConsensusV42] = v42for the promoted block. - Add
vN.ApprovedUpgrades[protocol.ConsensusV42] = 208000(208000 = 7-day delay). - The new vFuture block starts from
v42and carries onlyLogicSigVersion = L+1. No feature params — they were all promoted.
Step 2b: data/transactions/logic/opcodes.go — LogicVersion & experimental opcodes
Bumping vFuture.LogicSigVersion to L+1 means the AVM must actually support version
L+1, so opcodes staged for the next release have a home. Bump:
const LogicVersion = 14 // was 13
Now deal with the experimental opcodes. There is a small experiments list (in
assembler_test.go) guarded by TestExperimental, which is designed to fail when
vFuture.LogicSigVersion increments. Each experimental opcode has a version const near
the top of opcodes.go (e.g. sumhashVersion) currently equal to L. For each one,
decide with the user:
- Release it: it ships in the new numbered version. Remove it from
experiments; its version const staysL. Simplest path — no further test churn. - Hold it experimental: it belongs in the next dev version. Bump its version const
to
L+1. This is whyLogicVersionhad to move toL+1— otherwise the opcode would reference a version the assembler cannot target. Holding a stateless opcode back ripples into its own tests (see Step 5).
Do not bump docVersion (Step 4) — the published langspec must stop at the released
version L.
Step 3: ledger/testing/consensusRange.go
Add the new version to consensusByNumber, immediately before ConsensusFuture, with
a short comment summarizing what it houses:
protocol.ConsensusV41, // AVM v12, txn access, Sha512BlockHash, AppVersioning
protocol.ConsensusV42, // AVM v13, <summary of promoted features>
protocol.ConsensusFuture,
This is what makes versionStringFromIndex/TestConsensusRange map the new number to
a real version instead of "vFuture".
Step 4: Regenerate the language spec
touch data/transactions/logic/fields_string.go
make -C data/transactions/logic
git diff --stat data/transactions/logic — expect langspec_v*.json LogicSigVersion
bumps (L-1 -> L). If you held an experimental opcode back to L+1, it also drops
out of the released version's langspec and TEAL_opcodes_v<L>.md. There should be no
new langspec_v(L+1).json — that only appears once docVersion is bumped in a later
release, so verify one wasn't generated.
Step 5: Build and fix broken tests
make buildsrc
go test ./config/... ./ledger/testing/... ./data/transactions/logic/... ./ledger/...
Failures fall into two kinds. Diagnose which before "fixing".
(a) Tests pinned to Current/Future identity
A test asserts "this feature is on/off" by comparing to ConsensusCurrentVersion or
ConsensusFuture, and the assertion flips when Current moves. Rewrite it to depend on
the numeric version, so it keeps meaning the same thing after the next promotion:
TestConsensusRange(t, start, stop, fn)(ledger/testing) when the claim is "works from version X onward";startis the numeric index,stopof0means "through vFuture".- Otherwise parameterize the body over specific versions pulled from
consensusByNumber[i]— typically the boundary pair (last version without the feature, first with it).
(b) Tests that break because Current gained a feature
Current now enables something vFuture used to (this promotion: LoadTracking, which
requires validated blocks to carry a computed Load header field). Tests that
hand-build blocks, or otherwise construct protocol data structures directly, now omit a
newly-required field and fail (e.g. bad load: 0 != N).
Do NOT pin these to an old numeric version to dodge the feature. That hides the feature from the test forever and defeats the point. Instead:
- Build the data properly. Prefer the shared test helper over inlined
construction. Here,
ledger/ledger_test.go'sendOfBlockhelper is "a simplifiedBlockEvaluator.endOfBlockso our test blocks can pass validation" — teach it to set the new gated field, and have the offending tests call it instead of inliningPaysetCommit/FeesCollected:if proto.LoadTracking { blockTxBytes := 0 for i := range blk.Payset { blockTxBytes += blk.Payset[i].GetEncodedLength() } blk.BlockHeader.Load = eval.ComputeLoad(blockTxBytes, proto.MaxTxnBytesPerBlock) } - Run the test against Current AND Future, so the next vFuture feature breaks it
during development rather than at release:
with the body parameterized onfor _, cv := range []protocol.ConsensusVersion{protocol.ConsensusCurrentVersion, protocol.ConsensusFuture} { t.Run(string(cv), func(t *testing.T) { testFooImpl(t, cv) }) }cv(used forconfig.Consensus[cv]andGenerateInitState(t, cv, ...)). The guiding principle: tests should already exercise vFuture, so that promotion is a non-event. A test that only ran at Current is why this break surfaced at release time instead of when the feature landed.
Always propose test changes to the user rather than rewriting silently — the start
version, the boundary, and the "build it properly" fix are judgment calls.
The TestExperimental gate and the autosalt ripple (both expected)
TestExperimental(data/transactions/logic/assembler_test.go) fails by design whenvFuture.LogicSigVersionincrements. Resolve per Step 2b (release, or hold and bump the version const).- Held-back experimental opcodes ripple into their own scaffolding: their opcode
moves out of the released version's assembly, so the per-version golden tables in
assembler_test.goneed a newv(L+1)Nonsense/v(L+1)Compiled(and map entries), and per-version programs ineval_test.go(globalV(L+1)TestProgram+TestAllGlobalsmap;testTxnProgramTextV(L+1)+TestTxnmap) need a v(L+1) entry (usually identical to vL, since no new fields). A dedicated test likeTestSumhashthat hardcoded the opcode's version should reference the version const instead. WhenL+1introduces no encoding change,v(L+1)Compiled = vLCompiled + <moved-op bytes>. - Autosalt fragility (
LogicSigOffCurveVersion). BumpingLogicVersionchanges the program version byte, which changes every stateless program's hash, which flips whether it hashes on-curve. AtLogicSigOffCurveVersionand above, a stateless (non-app) program that lands on-curve gets an auto-saltintcblockconstant appended by the assembler. That silently changes assembled bytes (breaking golden-hex assembler tests) and adds an opcode of cost (breaking exact opcode-budget assertions, e.g. an off-by-one inTestIncrementCheck). Tests broken this way are stateless programs whose exact bytes/cost matter; fix each by disabling the salt with a#pragma autosalt falseline at the top of the program source. (Stateful programs — those with app/box opcodes — never auto-salt, soTestAssemble's nonsense, which includesapp_box_*, is immune.)
Step 6: Verify
- The linchpin:
It checks the version below vFuture has nogo test ./ledger/testing/ -run TestReleasedVersion -vApprovedUpgrades, vFuture'sLogicSigVersionis strictly greater than the released one, and everyconsensusByNumberentry resolves to real params. Failure means Step 2a or 3 is incomplete. go test ./data/transactions/logic/ -run 'TestExperimental|TestReleased|TestAssemble'and the Step 5 packages.git status --porcelain— the changed set should be roughly:config/consensus.go,protocol/consensus.go,ledger/testing/consensusRange.go,data/transactions/logic/opcodes.go, generateddata/transactions/logic/langspec_v*.json(+TEAL_opcodes_v<L>.md), plus the test files you repaired. If unsure whether generated output is complete, consultscripts/travis/codegen_verification.sh.
Notes
- New/changed consensus parameters warrant a spec update, but that is the
spec-consensus-paramskill's job and was presumably done when the params first landed in vFuture. This skill is only the promotion mechanics. - Do not commit or open a PR unless asked; leave the tree for review. If a placeholder URL was used, call it out explicitly in your summary so it is not forgotten.
- Per
../CLAUDE.md, a PR takes a single-word area prefix (e.g. "Consensus:") and an "enhancement" tag.