docs: add Antelope-to-Wire contract migration guide - #111
Conversation
huangminghuang
left a comment
There was a problem hiding this comment.
Reviewed the complete diff at 54b55e7. CI is green, but I found several correctness issues in the migration path, including an unrunnable provisioning command and source/API compatibility claims that do not hold. Inline comments contain reproductions and suggested corrections, so I am not approving this revision. As the PR body notes, the cross-repo ROA link also requires Wire-Network/wire-sysio#583 to merge first.
Smaller accuracy corrections: docs/migrating-from-antelope.md:71 has -wasm2wast where the Antelope command is eosio-wasm2wast; line 85 says -bare emits only the header/source skeleton although it also emits the contracts Markdown and README files; line 428 groups sha3 under the assert_ forms although no assert_sha3 is exposed; and the README summary at line 42 should say the contract is billed by default so it does not erase the explicit-payer exception.
|
|
||
| > **If AntelopeIO CDT 3.0+ is already installed**, both packages publish a `cdt-cpp` (and `cdt-cc`, | ||
| > `cdt-ld`, `cdt-init`) on `PATH`. Install Wire CDT from the portable tarball to `/opt/wire-cdt` and | ||
| > put `/opt/wire-cdt/bin` ahead on `PATH` for Wire work, or check `which cdt-cpp` before every build. |
There was a problem hiding this comment.
The tarball setup contradicts the isolation guarantee above. InstallCDT.cmake installs unprefixed clang, clang++, lld, ld.lld, opt, llc, wasm-ld, and the LLVM tools into this same bin, so putting /opt/wire-cdt/bin first shadows the distro tools. Please scope lines 41–43 to deb/rpm and recommend an absolute cdt-* path, alias, or public-entry-only symlink directory for a coexisting tarball.
There was a problem hiding this comment.
Confirmed and fixed in a951e0a. InstallCDT.cmake:59 installs unprefixed clang, clang++, lld, ld.lld, opt, llc, wasm-ld and the llvm-* set into the same bin/, so my advice was actively harmful for the tarball.
The isolation guarantee is now scoped to deb/rpm, where only public entry points are symlinked into /usr/bin. The tarball gets its own callout saying plainly that its bin/ holds everything and that putting it on PATH shadows the distro toolchain, with the three safe options: absolute path, alias, or a private symlink dir holding just the cdt-* names. The AntelopeIO name collision moved into that callout too, since the fix is the same.
| > put `/opt/wire-cdt/bin` ahead on `PATH` for Wire work, or check `which cdt-cpp` before every build. | ||
|
|
||
| ```bash | ||
| sudo apt install ./wire-cdt_<version>_amd64.deb # deb / rpm → /usr/lib/cdt |
There was a problem hiding this comment.
This installs only the base component. The native-test workflow linked at line 128 needs libnative*, gen_native_dispatch.py, and share/cdt/native-contract-src, all packaged in wire-cdt-dev; CMake and a build tool are not base-package dependencies either. On a clean machine the later build/native-test steps are therefore unavailable. Please install/document wire-cdt-dev, CMake, and the build tool, or state that those steps assume them.
There was a problem hiding this comment.
Confirmed and fixed in a951e0a. libnative*, scripts/gen_native_dispatch.py and share/cdt/native-contract-src are all COMPONENT dev, and neither package depends on CMake or a build tool — so a reader following the page top to bottom would hit "Test without a chain" with nothing to run it.
The install step now takes both packages and adds cmake ninja-build, with a sentence saying which package carries what and why the dev one is needed for the native path further down.
|
|
||
| ```bash | ||
| clio push action sysio.roa addpolicy \ | ||
| '{"owner":"mycontract","issuer":"<nodeowner>","netWeight":"0.1000 SYS", \ |
There was a problem hiding this comment.
This mandatory command cannot be parsed as written for two independent reasons. The line-ending backslash is inside the still-open single-quoted JSON string, so the shell passes a literal backslash/newline; and the ABI fields are snake_case (net_weight, cpu_weight, ram_weight, time_block, network_gen), not camelCase. Current clio convert pack_action_data fails with Missing field 'net_weight'. Please keep the JSON on one quoted line (or use a safe file/input form) and use the ABI field names.
There was a problem hiding this comment.
Confirmed and fixed in a951e0a — and the camelCase was my own regression. The original draft had snake_case; I "corrected" it against sysio.roa.hpp, whose parameters are netWeight etc., without checking that contracts/sysio.roa/sysio.roa.abi declares net_weight / cpu_weight / ram_weight / time_block / network_gen. Good catch.
The command is now a single line with the ABI names, plus a note on both traps: the ABI spelling is not the C++ parameter spelling, and a wrapping \ lands inside the single quotes rather than continuing the line.
| -p <nodeowner>@active | ||
| ``` | ||
|
|
||
| Without it, every call fails with `account mycontract net usage is too high: 132 > 0` — which looks |
There was a problem hiding this comment.
Please scope this to ordinary/default contract-paid calls. A provisioned caller or relayer can opt in as the explicit CPU/NET payer using an index-0 sysio.payer authorization plus the matching real authorization, so it is not true that nobody can call the contract or that every call fails when the contract itself has zero CPU/NET.
There was a problem hiding this comment.
Agreed, fixed in a951e0a. It also contradicted the ROA page I link to, which says an unprovisioned contract is "inert for ordinary users, not universally inert".
Now scoped: an ordinary call fails, and because billing keys on the payer alone, a caller naming itself with sysio.payer pays for the action and never consults the contract's zero limits — so a provisioned caller or relayer can still drive it.
|
|
||
| ```bash | ||
| grep -rl 'eosio\|EOSIO\|EOSLIB' src include \ | ||
| | xargs sed -i -e 's/\beosio\b/sysio/g' -e 's/\bEOSIO_/SYSIO_/g' -e 's/\bEOSLIB_/SYSLIB_/g' |
There was a problem hiding this comment.
The word boundary prevents exactly the C API renames listed above: _ is a word character, so \beosio\b does not match the eosio prefix in eosio_assert or eosio_exit. Those identifiers remain unchanged. Please add explicit substitutions (or a token-aware rename) so the advertised first pass covers them.
There was a problem hiding this comment.
Confirmed and fixed in a951e0a. _ is a word character, so \beosio\b has no boundary to match in eosio_assert — the recipe skipped precisely the C API entries listed in the table right above it.
Verified both directions on a fixture containing eosio_assert, eosio_assert_message, eosio_exit, eosio::name, EOSIO_DISPATCH and EOSLIB_SERIALIZE: the old recipe leaves the three C API lines untouched, the new one converts all six. The added expression names them explicitly, and the text says why it is not redundant.
|
|
||
| ### `multi_index` still works | ||
|
|
||
| `sysio::multi_index` is a drop-in replacement implemented over the KV intrinsics. It keeps |
There was a problem hiding this comment.
multi_index is not fully source-compatible: Wire explicitly deletes postfix operator++(int) and operator--(int) on both primary and secondary iterators, while Antelope provides them. Common loops such as for (...; it != table.end(); it++) fail to compile (this repo's existing examples use that form). Please document the required it++ → ++it / it-- → --it migration or restore those operators, and soften “drop-in” / “usually does not change at all.”
There was a problem hiding this comment.
Confirmed and fixed in a951e0a. operator++(int) and operator--(int) are = delete at kv_multi_index.hpp:422-423 (primary) and 807-808 (secondary).
"Drop-in" is now "near-drop-in", the TL;DR row says to expect a mechanical it++ → ++it sweep, and there is a short subsection showing the failing loop, the rewrite, and why a postfix increment cannot work when the iterator owns a host-side handle. Added to the porting checklist as its own step.
Worth flagging separately: examples/multi_index_example and examples/multi_index_large use itr++ in five places, so those examples cannot compile as written — they are evidently not built by CI. I have left them alone as out of scope for a docs PR, but they look like a real breakage.
| preserved. | ||
|
|
||
| Secondary key types carried over: `uint64_t`, `uint128_t`, `double`, `long double`, and | ||
| `checksum256` (and anything else with a CDT serializer, via the generic encoder). Iteration order |
There was a problem hiding this comment.
The generic encoder does not make every serializable type usable here. secondary_index_view has a std::is_trivially_copyable static assertion, so serializer-supported types such as std::string and std::vector are rejected. Please state the actual constraint rather than “anything else with a CDT serializer.”
There was a problem hiding this comment.
Confirmed and fixed in a951e0a. The static_assert(std::is_trivially_copyable<secondary_key_type>::value, ...) at kv_multi_index.hpp:769 is the real constraint, and "anything else with a CDT serializer" claimed the opposite of what it enforces.
The text now states the actual rule and notes that std::string / std::vector keys are rejected at compile time despite being serializable, with the practical workaround — hash to a checksum256, or truncate to uint64_t and disambiguate against the primary row. Also added as a checklist step.
| kv::table<"user_balance_history"_i, my_key, my_val> users(get_self()); | ||
| ``` | ||
|
|
||
| Annotate the value struct with `[[sysio::table("user_balance_history")]]` so the ABI carries the |
There was a problem hiding this comment.
This recipe is unsafe for annotated _i table names of 13 characters or fewer. Runtime _i always DJB2-hashes the literal, but abigen::add_table sends short annotated names through legacy string_to_name; the runtime and ABI table_id then differ (for example, user_table is 61956 at runtime versus 3509 in the ABI). RPC metadata can point at the wrong table. Please fix ABI generation first or document/restrict the safe naming case.
There was a problem hiding this comment.
Confirmed and fixed in a951e0a — and I reproduced your figures independently before changing anything. abigen.hpp:297-306 branches on t.name.size() <= 13, taking string_to_name for short names, while _i in hash_id.hpp DJB2-hashes unconditionally:
| name | runtime (_i) |
ABI |
|---|---|---|
user_table (10) |
61956 | 3509 |
accounts (8) |
64120 | 25660 |
user_balance_history (20) |
26461 | 26461 |
So it is every short annotated name, not an edge case.
I documented the restriction rather than attempting the fix here, since changing add_table moves the table_id of any contract already using that path and is a breaking ABI change that wants its own PR and a pinned toolchain test. The guide now says to use _i only above 13 characters — where both sides hash and agree, which is the case _i exists for — and _n below, where both use string_to_name and likewise agree. The table above is in the page so the failure mode is recognisable.
Happy to open the codegen fix as a follow-up if you would rather have it than the restriction.
| works at all. | ||
|
|
||
| **On Wire, the account billed for an action's CPU and NET is the contract that action invokes, not | ||
| the account that signed it.** An ordinary transaction names no payer, so the signer is neither |
There was a problem hiding this comment.
Please qualify this as objective billing. Subjective billing is enabled by default for API/P2P traffic, and the pre-execution check applies the subjective failure/CPU budget to non-payer first authorizers. Thus the signer has no objective CPU/NET charge, but can still be limit-checked and throttled after failed attempts.
There was a problem hiding this comment.
Confirmed and fixed in a951e0a. verify_init_subjective_billing takes set_difference(first_authorizers, payers) and, for each authorizer that is not a payer, throws tx_cpu_usage_exceeded / greylist_cpu_usage_exceeded when the subjective budget is gone — so the signer is very much limit-checked, just not by consensus.
The claim is now scoped to objective billing, with a following paragraph on subjective billing metering the first authorizer, that it is node-local rather than consensus, and that a signer failing transactions in a loop can be throttled.
|
|
||
| CPU is metered per **top-level** action — timed across that action's entire execution, including | ||
| every inline action and notification handler it triggers — and billed to that top-level action's | ||
| payer. NET is likewise charged per top-level action, on the transaction's serialized bytes; inline |
There was a problem hiding this comment.
NET is not based on the whole transaction's serialized bytes. packed_transaction::get_action_billable_size() charges a fixed per-action overhead plus only that top-level action's serialized billable size (and its matching context-free data when applicable); it excludes headers, signatures, and other actions. Please describe that per-action formula, since it changes the provisioning/sizing guidance.
There was a problem hiding this comment.
Half agreed — the sentence was wrong and is fixed in a951e0a, but I do not think the replacement formula is right, so flagging rather than adopting it silently.
get_action_billable_size is indeed per-action, and my "on the transaction's serialized bytes" was wrong. But the overhead it adds is not a fixed constant excluding headers and signatures — it is billable_net_per_action_overhead, computed in packed_transaction::init:
int64_t size = config::fixed_net_overhead_of_packed_trx; // 16
size += fc::raw::pack_size(signatures);
size += fc::raw::pack_size(unpacked_trx.transaction_extensions);
size += fc::raw::pack_size(static_cast<const transaction_header&>(unpacked_trx));
billable_net_per_action_overhead = (size / unpacked_trx.total_actions()) + 1;Signatures, extensions and the header are all in there; they are apportioned across the actions rather than excluded. Only other actions' payloads are excluded, which is the part your comment gets right and my sentence got wrong.
That distinction is exactly what changes the sizing guidance, so the page now gives the full formula:
action NET = that action's own serialized billable size
+ (16 + signatures + extensions + header) / number of actions, rounded up
+ its matching context_free_data, for a context-free action
with the two consequences that follow: signatures cost NET, so co-signing is dearer; and batching amortizes the overhead. This also matches wire-sysio's docs/roa-overview.md, which the page links to. If I have misread init(), say so and I will correct it again.
Eleven review findings on #111, each verified against the code before the text changed. Several were factual errors, not imprecision. Toolchain install. The tarball has no public-entry separation: InstallCDT.cmake puts unprefixed clang, clang++, lld, ld.lld, wasm-ld, opt, llc and llvm-* in the same bin/, so the previous advice to put /opt/wire-cdt/bin on PATH shadowed the distro toolchain. That guarantee holds only for deb/rpm, where /usr/bin gets symlinks for public entry points alone; the tarball now recommends an absolute path, an alias, or a private symlink dir. `apt install` also installed only the base component, while the native-testing path linked further down needs libnative*, gen_native_dispatch.py and share/cdt/native-contract-src -- all COMPONENT dev -- and neither package pulls in CMake or a build tool. The addpolicy command could not run. Its line-continuation backslashes sat inside the still-open single-quoted JSON, and the fields were camelCase from the C++ parameters where sysio.roa.abi declares net_weight / cpu_weight / ram_weight / time_block / network_gen. One line, ABI names. multi_index is not the drop-in the guide claimed. Wire deletes operator++(int) and operator--(int) on both the primary and secondary iterators, so the classic `it++` loop stops compiling -- this repo's own examples/ still use that form. The required ++it sweep is documented and the "drop-in" framing softened. Secondary key types are constrained by a std::is_trivially_copyable static_assert in secondary_index_view, not by "has a serializer", so std::string and std::vector keys are rejected; the guide said the opposite. The `_i` recipe was unsafe for short names. Runtime `_i` always DJB2-hashes, but abigen routes an annotated name of <= 13 characters through legacy string_to_name, so the row is written under one table_id and described in the ABI under another (user_table: 61956 at runtime, 3509 in the ABI -- reproduced independently). Restricted to names over 13 characters, where both sides hash and agree. cdt-abidiff cannot show the change the guide pointed it at: it compares tables only by name and type, ignoring index_type, key_names, key_types, table_id and secondary-index metadata, and its version check reduces eosio::abi/1.2 and sysio::abi/1.2 to the same number. Replaced with a normalized jq diff. The rename recipe skipped what it advertised. `_` is a word character, so \beosio\b finds no boundary in eosio_assert and left the whole C API unrenamed -- the exact identifiers the table above it lists. Those are now named explicitly. Billing claims scoped. "The signer is neither charged nor limit-checked" is true only of objective billing: verify_init_subjective_billing applies a subjective CPU budget to first authorizers who are not payers and can terminate the transaction. NET is not the transaction's serialized bytes either -- each action is billed its own billable size plus an equal share of a 16-byte constant, the signatures, the extensions and the header, so signatures cost NET and batching amortizes overhead. The porting checklist gains the iterator sweep, the secondary-key constraint, the C API rename and the -dev package, and no longer recommends cdt-abidiff.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed the complete PR and the follow-up delta at a951e0a. The follow-up resolves the original 11 inline findings and all current-head CI checks are green, but I found additional compatibility and resource-model issues, including a state-integrity break in the current multi_index::emplace shim. Inline comments have the implementation evidence and requested wording, so I am not approving this revision.
The PR description also needs the required full-diff refresh: it still calls multi_index a drop-in shim requiring no source changes, contrary to the follow-up commit and the current guide. Smaller corrections remain at docs/migrating-from-antelope.md:89 (eosio-wasm2wast, not -wasm2wast), line 103 (cdt-init -bare also emits the contracts Markdown and README), and line 167 (the C++ addpolicy parameters are snake_case too, not camelCase). Keep the documented merge ordering behind Wire-Network/wire-sysio#583; that PR is still open and the master-branch ROA link still does not resolve.
Correction to my prior review body: sysio::assert_sha3 does exist as a library wrapper in crypto_ext.hpp, so that earlier minor note was wrong and is withdrawn.
| below needs. Neither package pulls in CMake or a build tool, so on a clean machine also: | ||
|
|
||
| ```bash | ||
| sudo apt install cmake ninja-build # or build-essential for make |
There was a problem hiding this comment.
These clean-machine prerequisites are not sufficient for the commands the guide gives. This installs Ninja, but line 99 invokes the default generator and make; line 253 newly requires jq, which is not installed here. Please either install build-essential jq, or select Ninja consistently (-G Ninja / cmake --build) and add jq.
There was a problem hiding this comment.
Fixed in 2653497. The prereq line installed Ninja while the guide then ran the default generator and make, and never installed jq at all. Now sudo apt install cmake build-essential jq, with a sentence saying which command each is for — keeping make, since that is what the cdt-init project generates.
|
|
||
| **A new account holds zero CPU and zero NET.** For a user account that does not matter — see | ||
| [Step 3](#step-3--resources-the-contract-pays). For the account that will *hold your contract* it is | ||
| fatal, because on Wire the contract is the payer. Before anyone can call it, a node owner must |
There was a problem hiding this comment.
The later paragraph now documents the explicit-payer exception correctly, but this sentence (and the introduction at line 5) still says nobody can call an unprovisioned contract. Please say before an ordinary/default contract-paid call. Also, the explicit payer bypasses only the contract's zero CPU/NET; any new RAM the contract bills to itself still needs RAM headroom.
There was a problem hiding this comment.
Fixed in 2653497, at both places — the introduction now says the contract is what has to be provisioned "before ordinary calls will go through", and the deploy section says "ordinary contract-paid call".
The RAM point is a good catch and I have added it explicitly: the sysio.payer route bypasses the contract's zero CPU/NET only, so RAM the contract bills to itself still comes out of its own quota — an unprovisioned contract can be driven no further than its first write to its own tables.
| issue it a policy: | ||
|
|
||
| ```bash | ||
| clio push action sysio.roa addpolicy '{"owner":"mycontract","issuer":"<nodeowner>","net_weight":"0.1000 SYS","cpu_weight":"0.1000 SYS","ram_weight":"1.0000 SYS","time_block":0,"network_gen":0}' -p <nodeowner>@active |
There was a problem hiding this comment.
Hard-coding network_gen:0 is not valid for a generic shared network. addpolicy opens nodeowners in the supplied generation and requires the issuer there; after a generation rollover, a valid issuer registered elsewhere fails or this draws from an old generation's allocation. Use a placeholder and explain how to query/select the issuer's intended generation.
There was a problem hiding this comment.
Fixed in 2653497. addpolicy opens nodeowners scoped to the generation passed and requires the issuer there, so a literal 0 is wrong on any network that has rolled over.
It is now <gen>, with a paragraph explaining it is the generation the issuer is registered in and two commands to determine it — read network_gen from the roastate singleton, then confirm the issuer appears in that generation's nodeowners.
|
|
||
| ### `multi_index` still works | ||
|
|
||
| `sysio::multi_index` is a near-drop-in replacement implemented over the KV intrinsics. It keeps |
There was a problem hiding this comment.
near-drop-in is still unsafe as written. Wire emplace calls kv_set without kv_contains; when the primary key already exists, the chain takes its update branch, whereas legacy db_store_i64/emplace rejects the duplicate. If the secondary key changed, store_secondaries then adds the new mapping without removing the old one, leaving stale index state. Separately, primary lower_bound/upper_bound accept only uint64_t, so code using a name primary key no longer compiles while upstream accepts its templated PK. Please fix the shim or explicitly document both safeguards/conversions, and remove the one source change framing.
There was a problem hiding this comment.
Both confirmed, and I fixed the shim rather than documenting around it — #113.
The orphan reproduced exactly as you described, and I demonstrated it against the real runtime before changing anything: emplace pk=1 sec="aaa", emplace pk=1 sec="bbb", then look up "aaa".
before: ORPHAN: stale secondary A still resolves after duplicate emplace
after: object with the same primary key already exists
emplace now checks kv_contains first, the way kv::table::emplace already did — the compat shim was the one missing what the perf-first wrapper pays for. lower_bound/upper_bound are templated on the primary key type like upstream's to_raw_key; making that work surfaced three further sites passing primary_key() straight into pk_to_bytes, which now use the same to_pk_uint64 conversion.
Here in 2653497: the "one source change" framing is gone, multi_index is a compatibility shim rather than near-drop-in, and the section states that duplicate-emplace and the bounds signatures match upstream. This PR now depends on #113.
| > | `user_balance_history` (20 chars) | 26461 | 26461 ✓ | | ||
| > | ||
| > Above 13 characters both sides hash, so they agree — which is the case `_i` exists for. Short | ||
| > names should use `_n`, where runtime and ABI both use `string_to_name` and likewise agree. |
There was a problem hiding this comment.
The guide's own user_table example cannot follow this advice: _n rejects _ (its alphabet is .12345a-z, with an additional 13th-character restriction), so it is a compile-time error. Only short identifiers that are valid Antelope names can switch to _n; other short _i identifiers must be renamed/lengthened until abigen is fixed.
There was a problem hiding this comment.
Confirmed and fixed in 2653497 — the fallback I offered was impossible for the very example above it. basic_name's alphabet is .12345a-z and symbol() calls sysio::check on anything outside it, which in a constexpr _n literal is a compile error, so "user_table"_n never builds.
The callout now says _n is the fix only for short names that are already valid Antelope names, and that a short identifier containing _, a digit outside 1-5, or an uppercase letter has to be renamed or lengthened past 13 characters until abigen is fixed.
|
|
||
| ``` | ||
| action NET = that action's own serialized billable size | ||
| + (16 + signatures + extensions + header) / number of actions, rounded up |
There was a problem hiding this comment.
This is presented as an exact formula, but consensus computes integer division followed unconditionally by +1: floor(overhead / action_count) + 1. That differs from mathematical ceiling whenever the overhead divides evenly (including every one-action transaction), understating each action by one byte. Please show the actual integer formula.
There was a problem hiding this comment.
Agreed that "rounded up" was wrong, and fixed in 2653497 — but I read the direction the other way, so flagging rather than adopting it silently.
billable_net_per_action_overhead = (size / unpacked_trx.total_actions()) + 1 is floor-then-+1. When the division is exact, floor(x/n) + 1 is one more than ceil(x/n), so each action is billed a byte over, not under. transaction.cpp:380 agrees:
// +1 rounds up so per-action shares sum to at least total overhead; may over-bill
// by up to total_actions() bytes per trx (negligible).The guide now shows the arithmetic in two steps rather than calling it a ceiling, and says it over-bills by up to a byte per action where the division is exact. If I have the sign backwards I will correct it again — but the code comment reads the same way I do.
| - A contract notifies your `on_notify` handler. The top-level payer pays your handler's CPU; your | ||
| own policy covers only the RAM your handler writes. | ||
|
|
||
| The CPU and NET a contract must be provisioned for is therefore the cost of everything its actions |
There was a problem hiding this comment.
Only CPU has the whole-call-tree property. As line 453 correctly says, inline actions and notifications add zero NET; NET consists of the input action's bytes, its apportioned transaction overhead, and matching context-free data. Please split this conclusion so it does not say a contract's NET provisioning covers everything the action causes.
There was a problem hiding this comment.
Confirmed and fixed in 2653497. The conclusion had generalised a CPU property to NET, contradicting the sentence four lines above it.
It now says explicitly that only CPU has to cover everything the actions cause, since the whole tree is timed against the top-level payer, while NET is fixed by what reaches the wire — the input action's bytes, its share of the overhead, and its context-free data — and no inline action or notification adds to it however deep the tree goes.
|
|
||
| | Function(s) | What to do instead | | ||
| |---|---| | ||
| | `db_store_i64`, `db_update_i64`, `db_remove_i64`, `db_get_i64`, `db_next_i64`, `db_previous_i64`, `db_find_i64`, `db_lowerbound_i64`, `db_upperbound_i64`, `db_end_i64` | Use `multi_index` (unchanged source) or the `kv_*` intrinsics. | |
There was a problem hiding this comment.
unchanged source contradicts the mandatory iterator rewrite above, the name-primary bounds mismatch, and the fact that direct db_* callers necessarily must be rewritten. Please point readers to the compatibility adjustments instead.
There was a problem hiding this comment.
Fixed in 2653497. The checklist entry now says direct db_*_i64 / db_idx* callers must be rewritten, and that multi_index users need only the iterator sweep in the preceding step. The "unchanged source" phrasing in the removed-intrinsics table is replaced with a pointer to the compatibility adjustments in Step 2.
|
|
||
| > Porting a contract from EOS/Telos/WAX or another Antelope chain? Start with | ||
| > [Migrating a Contract from an Antelope Chain to Wire](migrating-from-antelope.md) — `multi_index` | ||
| > is a drop-in shim there, so this section is the optional second step. |
There was a problem hiding this comment.
This new cross-link still says drop-in, contradicting the migration guide's postfix caveat and the additional verified emplace/name-primary differences. Please call it a compatibility shim or near-drop-in and link the required adjustments rather than promising unchanged behavior.
There was a problem hiding this comment.
Fixed in 2653497. That cross-link now calls multi_index a compatibility shim rather than a drop-in, names the required it++ → ++it sweep as the reason, and tells the reader to make those adjustments before treating the KV migration as an optional second step.
| [docs/migrating-from-antelope.md](./docs/migrating-from-antelope.md) walks through getting started on | ||
| Wire and porting an existing contract — the `eosio` → `sysio` renames, the KV storage layer that | ||
| replaces the legacy `db_*_i64` tables, the host functions Wire adds and removes, and the resource | ||
| model change that matters most: on Wire the **contract** is billed for CPU, NET and RAM, not the |
There was a problem hiding this comment.
The current guide now documents sysio.payer, so this summary should say the contract is billed by default. RAM is also selected by the contract's payer argument rather than categorically never landing on the signer.
There was a problem hiding this comment.
Fixed in 2653497. The README summary now says the contract is billed for CPU and NET by default rather than the signer, and that RAM follows whichever account the contract names as payer instead of categorically never reaching the signer.
|
Also addressed the four items from the review body, in 2653497:
PR description refreshed for the full diff, as required. It no longer calls This PR now depends on #113, which fixes the two code defects from your Thanks for both rounds — the stale-repo one in particular caught a mistake I had introduced while fixing an earlier finding. |
Final review round found that nothing in the tree pins the one fact receiving_account() depends on. Changing cdt-codegen.cpp:83 from sysio_set_contract_name(r) to (c) leaves ctest 30/30 and the integration suite 24/24 green: every in-tree action is self-sent, so r == c, and the native test drives the global directly rather than through apply(). The divergence appears only under notification, on chain -- and the one downstream contract that would catch it, wire-sysio's ram_restrictions_test, is not rebuilt by that repo's CI (SYSIO_BUILD_TEST_CONTRACTS: "OFF"). dispatch_receiver_tests.sh inspects the emitted dispatch text, which no other test looks at: it asserts the call passes `r`, and that it precedes any action dispatch. Registered under unit_tests so it runs in required CI. Verified to gate: with the argument changed to `c` it fails and everything else still passes. Also from the same review: - The header comment added in the previous commit cited docs/kv-multi-index.md for the divergences, but this PR touches no docs -- those edits are on the #111 branch, and on THIS branch that file still calls the shim a drop-in replacement. The divergences are listed inline instead, and the comment now records that sysio::multi_index and sysio::singleton are both aliases of this template, so the new guards reach the singleton surface too. - The foreign-code case's `rows.count(seeded) == 1` cannot fail: the mock installs no kv_erase and kv_set only assigns, so nothing can reduce the count. The value comparison beside it is what carries the property; the comment said otherwise and now says which is which.
|
Reviewed against merged One claim had gone falseThe Unchanged host functions list promised "every The header is gone outright in the first case; in the second Tense, where it changed the adviceThe ABI section told readers not to reach for The legacy-database and One gap in both docsThe #113 receiver guard reaches Verified, not rewordedRebuilt CDT at merged master and compiled the claims:
The RAM tables, Still worth doing before merge: this branch is 57 commits behind |
huangminghuang
left a comment
There was a problem hiding this comment.
Reviewed current head 7ed8f269. CI is green, but I found remaining accuracy and portability issues, so I cannot approve this revision yet. The inline comments below are all against the current head.
Please also refresh the PR description after the follow-up commits: #113 is now merged, key_names/key_types are upstream ABI fields rather than Wire additions, and the _i/example impact statements no longer match this revision.
|
|
||
| **ABI requirement:** When using `_i`, annotate the value struct with `[[sysio::table("app_configuration")]]` so CDT generates the ABI entry. | ||
|
|
||
| > **Prefer `_n` for a global.** A `_i`-named `kv::global` with a matching `[[sysio::table]]` |
There was a problem hiding this comment.
[P2] Make the examples follow this warning. The page still says both literal forms work, shows the >13-character "app_configuration"_i form that this block says fails to link, and its complete example uses short "app_config"_i with a table annotation, which this block says emits mismatched ABI metadata and breaks readable get_table_rows. Please make the runnable examples use _n (and drop the annotation/hash include if no longer needed), reserving _i for a labeled limitation. Also qualify the name rule: position 13 only accepts .12345a-j, not the full alphabet.
There was a problem hiding this comment.
You were right that the examples contradicted the block — but chasing it turned up that the block itself was describing a toolchain bug, not a property of _i. Opened #115 to fix it rather than document it.
abigen's add_table() named each entry by decoding the raw template parameter, which is a DJB2 hash for _i, not a name encoding — so it emitted a garbage twin (idrzzw4ktxljf, 5p.tmiidfxofh) beside the annotated name. Under 13 characters that is the two mismatched entries this block describes; at or above it the two hash to one table_id and the link fails. add_kv_table() — the kv::table path — already preferred the [[sysio::table]] attribute, which is exactly why kv::table + long _i has always worked and is tested. multi_index and kv_multi_index were affected too.
So this page no longer tells readers _i is unsuitable. Changes in 64b40a9eb:
- the block is scoped to a pre-fix(abigen): keep the annotated table name for a
_i-named table #115 toolchain and says what it was, rather than presenting it as how_ibehaves; - the alphabet rule is qualified as you asked —
_ncovers up to 13 characters of.12345a-z, with position 13 restricted to.12345a-j, since it is encoded in 4 bits rather than 5. Confirmed inname.hpp:value & 0x0Fulland>> (i == 12 ? 60 : 59); - the examples use
_nwhere the name fits one, and thehash_id.hppinclude is dropped from the complete example, which no longer needs it.
One more thing fell out while making that example runnable. It emitted no ABI table entry at all — and neither does the shipped examples/kv_global_example. A namespace-scope struct carrying only [[sysio::table]] is not reachable from the contract, so abigen skips it; it needs to be declared inside the contract class, or to carry sysio::contract("...") beside the table attribute. The example is now in-class and emits ('appconfig', 46121), and the "ABI requirement" note states the reachability rule instead of implying the annotation alone is enough. examples/kv_global_example has the same defect and is not fixed here — it is out of scope for a docs PR, and I would rather raise it separately than smuggle it in.
| divergences are: | ||
|
|
||
| - the postfix iterator operators `it++` / `it--` are deleted, because copying a KV iterator | ||
| duplicates a host-side handle. Rewrite those to `++it` / `--it`; the compiler finds every one; |
There was a problem hiding this comment.
[P2] Reverse iteration escapes this compiler check. rbegin()/rend() expose std::reverse_iterator, whose postfix operator still compiles and copies the underlying KV handle. The main guide at lines 349-352 already explains this. Mirror that warning here and require a manual sweep of reverse loops instead of promising that the compiler finds every use.
There was a problem hiding this comment.
Confirmed, and the contradiction was internal — the main guide already says "The compiler does not find every one." while this page promised the opposite. Fixed: the bullet now says the compiler finds every direct use but not the reverse ones, spells out that rbegin()/rend() return a std::reverse_iterator whose postfix operators belong to the adaptor and are not deleted, gives the loop that compiles clean while doing the handle-duplicating copy, and requires a manual sweep.
|
|
||
| - the postfix iterator operators `it++` / `it--` are deleted, because copying a KV iterator | ||
| duplicates a host-side handle. Rewrite those to `++it` / `--it`; the compiler finds every one; | ||
| - the primary `lower_bound` / `upper_bound` take a `name` as well as a `uint64_t`, where |
There was a problem hiding this comment.
[P2] Document the actual overload source break. An upstream call such as t.template lower_bound<uint64_t>(k) (or upper_bound) is valid because those APIs are member templates, but Wire's concrete overloads reject that syntax; a wrapper convertible to both name and uint64_t can also become ambiguous. Current kv_multi_index.hpp comments document both cases. Add them here and to the porting checklist; the bare member-address example is not a Wire-only incompatibility.
There was a problem hiding this comment.
Confirmed by compiling it:
error: 'lower_bound' following the 'template' keyword does not refer to a template
That is a much better example than the bare member address, and you are right that the member-address case is not Wire-only — the guide even says upstream cannot deduce PK there either. Restructured into the two real breaks: the explicit template argument, and the dual-convertible wrapper ambiguity (where against a single uint64_t parameter it used to pick the uint64_t conversion). The member-address case is demoted to a parenthesis noting it fails on both sides, with the static_cast escape hatch kept.
| **The `name` overload landed in | ||
| [wire-cdt#113](https://github.com/Wire-Network/wire-cdt/pull/113)**; on a CDT built before it the | ||
| bounds took `uint64_t` only, and a `name` primary key needed `.value` at the call; | ||
| - secondary key types must be `std::is_trivially_copyable`, not merely serializable — a |
There was a problem hiding this comment.
[P2] Upstream does not accept arbitrary serializable secondary keys. Its index backend supports only uint64_t, uint128_t, double, long double, and checksum256/fixed_bytes<32>; those types are trivially copyable. The supported fixed type set is therefore a shared restriction. Wire differs mainly in when and how the static_assert diagnoses it, so please remove this false contrast.
There was a problem hiding this comment.
You are right, and the guide contains its own disproof: the "Removed on Wire" table lists the upstream secondary-index backend as exactly db_idx64_*, db_idx128_*, db_idx256_*, db_idx_double_*, db_idx_long_double_* — five families, the same fixed set, all trivially copyable. Removed the false contrast. The bullet now says the supported set is upstream's and that what differs is the diagnosis: Wire's static_assert sits in secondary_index_view, so it fires at the first get_index<...>() rather than at the declaration.
| Use `scoped_table` when your data is naturally partitioned by an account or category (e.g., token balances per account, proposals per proposer). Use `kv::table` when scope adds no value (singleton-like tables, global registries). | ||
|
|
||
| `scoped_table` is a drop-in replacement for `multi_index` with the same scope semantics but without the object cache overhead: | ||
| `scoped_table` is a source-compatible alternative to `multi_index` (note the payer moves to the first parameter of the mutators, see below) with the same scope semantics but without the object cache overhead: |
There was a problem hiding this comment.
[P2] This is not source-compatible. The replacement changes the template from <Name, T, ...> to <Name, K, V, ...>, splits the row/key representation, changes primary-key access and mutator signatures, and moves the payer. This file's own migration example shows the required rewrite. Call it a migration target or API-similar alternative rather than source-compatible.
There was a problem hiding this comment.
Confirmed — and as you say, the page's own worked conversion demonstrates it. Reworded to "a migration target for multi_index, not a drop-in", listing what the port actually costs: <Name, T, ...> → <Name, K, V, ...>, the row splitting into separate key and value types, primary-key access changing with it, and the payer moving to the first parameter of every mutator. The performance points that followed are now framed as what the rewrite buys.
| | Dense table, few scopes | 124 | 144 (**+16%**) | 136 (+10%) | | ||
| | Dense table + 1 index | 252 | 280 (+11%) | 264 (+5%) | | ||
|
|
||
| Measured across EOS mainnet contracts the net effect is a 2–6% saving. The full breakdown is in |
There was a problem hiding this comment.
[P2] Qualify the 2-6% result. The linked analysis calls this an estimate and obtains 2-6% only when excluding xsat. Including xsat, secondary storage is approximately break-even for kv::table and 5% higher for multi_index. Please say estimated 2-6% excluding xsat, or show both population results.
There was a problem hiding this comment.
Confirmed against the source: kv-ram-billing.md:130 says "On the EOS mainnet (excluding xsat), KV saves 2-6%", and §§ at lines 92/100 give both populations. Fixed to say estimated, excluding xsat, and to give the other side too — including it, secondary storage alone is break-even for kv::table and ~5% higher for multi_index, with the reason (legacy index256_object stores its 32-byte key inline; KV always pays a shared_blob offset pointer). 345M of the 355.6M idx256 entries come from xsat, which is why the population choice moves the number.
| `set_proposed_producers_ex`, `get_blockchain_parameters_packed`, | ||
| `set_blockchain_parameters_packed`, `set_privileged` — but not `set_kv_parameters_packed`, removed | ||
| above), `set_finalizers`, and the full | ||
| cryptographic surface — `sha1`/`sha256`/`sha512`/`ripemd160` with their `assert_` forms, plus `sha3` (which has no |
There was a problem hiding this comment.
[P2] assert_sha3 exists. Current crypto_ext.hpp defines sysio::assert_sha3, and the unit tests exercise it. The same header also exposes keccak and assert_keccak, which are omitted from this claimed full cryptographic surface. Preserve assert_sha3 and include the Keccak pair (or narrow the wording if this is not intended to be exhaustive).
There was a problem hiding this comment.
Confirmed — all four exist in crypto_ext.hpp and compile. Fixed in 64b40a9eb: the line now reads sha1/sha256/sha512/ripemd160 with their assert_ forms, then sha3 and keccak with theirs (assert_sha3, assert_keccak). The "no assert_ form, here or upstream" claim was simply wrong.
| need only the iterator sweep in step 4. | ||
| 6. Check secondary-index key types are `std::is_trivially_copyable`; give any `std::string` or | ||
| `std::vector` key a fixed-width surrogate. | ||
| 7. **Find every `emplace` / `modify` that names a user as payer.** Decide, per table, whether the |
There was a problem hiding this comment.
[P2] Audit singleton payer calls too. Existing ports commonly call singleton::set(value, user) or get_or_create(user, ...); kv_singleton forwards that payer into kv_multi_index::emplace/modify, so these calls hit the same missing-sysio.payer runtime rejection. Generalize this to every storage mutator accepting a payer and name the singleton calls explicitly.
There was a problem hiding this comment.
Confirmed — kv_singleton::set and get_or_create both forward their payer into _t.emplace / _t.modify, so they hit the same rejection. Checklist item 7 is generalized to "every storage mutator that names a user as payer" and names the singleton calls explicitly alongside emplace/modify.
| 7. **Find every `emplace` / `modify` that names a user as payer.** Decide, per table, whether the | ||
| contract absorbs the RAM (`get_self()`) or the client will supply `sysio.payer`. | ||
| 8. Replace any `send_deferred` with an inline action, an off-chain relayer, or a crank action. | ||
| 9. Regenerate the ABI and diff it — `jq -S . old.abi > a && jq -S . new.abi > b && diff -u a b`, |
There was a problem hiding this comment.
[P2] Current cdt-abidiff does compare this metadata. #112 is merged: diff_tables() now checks table_id, key_names, key_types, and secondary_indexes as well as type/index_type. Recommend current cdt-abidiff, keeping the raw JSON diff only as a fallback for pre-#112 installations. Also include abi_extensions if lines 299-302 are meant to enumerate every compared section.
There was a problem hiding this comment.
Both correct, and both were mine from the previous commit — I fixed the ABI section and left its duplicate in the checklist 400 lines later, which is the exact failure mode I keep flagging in these reviews.
Checklist item 9 now recommends cdt-abidiff and keeps the raw JSON diff as the pre-#112 fallback. And abi_extensions is added to the enumerated list — I had derived that list by grepping diff_*() calls, which misses it because it goes through diff_opaque_section("abi_extensions") at line 660 rather than a diff_abi_extensions().
| Under the deb/rpm layout the toolchain lives in `/usr/lib/cdt`, and only an enumerated list of | ||
| public entry points is symlinked into `/usr/bin`: `cdt-cc`, `cdt-cpp`, `cdt-ld`, `cdt-abidiff`, | ||
| `cdt-init`, `cdt-codegen`, `cdt-protoc`, `cdt-protoc-gen-zpp`, `cdt-pp`, `cdt-wast2wasm`, | ||
| `cdt-wasm2wast` and their `sysio-*` originals. The bundled `clang`, `lld`, `wasm-ld`, `opt`, `llc` |
There was a problem hiding this comment.
[P3] Most of these tools have no sysio-* original. The install rules retain sysio-* aliases only for pp, wast2wasm, and wasm2wast; there is no corresponding sysio-cc, sysio-cpp, sysio-ld, sysio-abidiff, sysio-init, sysio-codegen, or protobuf command. Name the three aliases explicitly so readers do not look for commands that are not installed.
There was a problem hiding this comment.
Confirmed — the installed bin/ has exactly three: sysio-pp, sysio-wasm2wast, sysio-wast2wasm. The text implied all eleven entry points had sysio-* originals. Now names the three and says explicitly there is no sysio-cc, sysio-cpp, sysio-ld, sysio-abidiff, sysio-init, sysio-codegen or sysio-protoc, so readers reach for the cdt- name.
Adds docs/migrating-from-antelope.md for developers porting a contract from EOS, Telos, WAX or another Antelope-family chain. Covers getting started on Wire (toolchain install, cdt-init, add_contract, native testing, provisioning and deploy), the eosio -> sysio rename surface, the storage change from the legacy db_*_i64 tables to the KV layer, the resource-model change, and a verified diff of the host-function surface. The resource section is deliberately scoped to what changes in contract code -- where a contract bills RAM, why sysio.payer welds RAM to CPU/NET, and that CPU/NET are billed per top-level action so a contract's policy funds the whole inline and notification tree beneath it. The model itself is left to wire-sysio's docs/roa-overview.md rather than restated here. Also broadens the CLion build-dir ignore to cmake-build-*/ and ignores prequel's local review state.
Eleven review findings on #111, each verified against the code before the text changed. Several were factual errors, not imprecision. Toolchain install. The tarball has no public-entry separation: InstallCDT.cmake puts unprefixed clang, clang++, lld, ld.lld, wasm-ld, opt, llc and llvm-* in the same bin/, so the previous advice to put /opt/wire-cdt/bin on PATH shadowed the distro toolchain. That guarantee holds only for deb/rpm, where /usr/bin gets symlinks for public entry points alone; the tarball now recommends an absolute path, an alias, or a private symlink dir. `apt install` also installed only the base component, while the native-testing path linked further down needs libnative*, gen_native_dispatch.py and share/cdt/native-contract-src -- all COMPONENT dev -- and neither package pulls in CMake or a build tool. The addpolicy command could not run. Its line-continuation backslashes sat inside the still-open single-quoted JSON, and the fields were camelCase from the C++ parameters where sysio.roa.abi declares net_weight / cpu_weight / ram_weight / time_block / network_gen. One line, ABI names. multi_index is not the drop-in the guide claimed. Wire deletes operator++(int) and operator--(int) on both the primary and secondary iterators, so the classic `it++` loop stops compiling -- this repo's own examples/ still use that form. The required ++it sweep is documented and the "drop-in" framing softened. Secondary key types are constrained by a std::is_trivially_copyable static_assert in secondary_index_view, not by "has a serializer", so std::string and std::vector keys are rejected; the guide said the opposite. The `_i` recipe was unsafe for short names. Runtime `_i` always DJB2-hashes, but abigen routes an annotated name of <= 13 characters through legacy string_to_name, so the row is written under one table_id and described in the ABI under another (user_table: 61956 at runtime, 3509 in the ABI -- reproduced independently). Restricted to names over 13 characters, where both sides hash and agree. cdt-abidiff cannot show the change the guide pointed it at: it compares tables only by name and type, ignoring index_type, key_names, key_types, table_id and secondary-index metadata, and its version check reduces eosio::abi/1.2 and sysio::abi/1.2 to the same number. Replaced with a normalized jq diff. The rename recipe skipped what it advertised. `_` is a word character, so \beosio\b finds no boundary in eosio_assert and left the whole C API unrenamed -- the exact identifiers the table above it lists. Those are now named explicitly. Billing claims scoped. "The signer is neither charged nor limit-checked" is true only of objective billing: verify_init_subjective_billing applies a subjective CPU budget to first authorizers who are not payers and can terminate the transaction. NET is not the transaction's serialized bytes either -- each action is billed its own billable size plus an equal share of a 16-byte constant, the signatures, the extensions and the header, so signatures cost NET and batching amortizes overhead. The porting checklist gains the iterator sweep, the secondary-key constraint, the C API rename and the -dev package, and no longer recommends cdt-abidiff.
Twelve inline findings plus four from the review body, each verified against the code first. Factual corrections. The addpolicy field-name note claimed a camelCase/snake_case divergence between the C++ action and its ABI -- there is none; I had read wire-system-contracts' copy of sysio.roa.hpp, abandoned on a 2025 branch, where the authoritative wire-sysio/contracts copy is snake_case on both sides. The note is gone rather than reworded. `-wasm2wast` becomes `eosio-wasm2wast`; `cdt-init -bare` emits four files, not two, since write_ricardian runs unconditionally; the clean-machine prerequisites now install build-essential and jq, which the guide's own `make` and `jq` commands needed; and `network_gen` is a placeholder with instructions for finding the issuer's generation, because addpolicy scopes nodeowners to the value passed and hard-coding 0 breaks after a rollover. The `_n` fallback for short `_i` names was itself impossible. `_n`'s alphabet is `.12345a-z`, so the guide's own `user_table` example is a compile error. Only short names that are already valid Antelope names can switch; others must be renamed or lengthened until abigen is fixed. Over-absolute claims scoped. An unprovisioned contract blocks ordinary contract-paid calls, not every call -- with the added caveat that the sysio.payer escape hatch covers bandwidth only, so RAM the contract bills itself still needs headroom. Separating user-paid RAM from contract-paid bandwidth is possible via a persistent `<contract>@sysio.code` delegation and an inline action, so that claim is now scoped to the direct top-level call shown. Subjective billing meters each top-level action's first authorizer, so several accounts can be throttled, not one signer. Billing mechanics. The NET overhead split is `overhead / actions + 1` -- integer division then an unconditional +1, which over-bills by up to a byte per action where the division is exact, rather than being a ceiling. And only CPU covers the whole call tree; NET is fixed by what reaches the wire, so the conclusion is split. Compatibility framing. multi_index is a compatibility shim, not a drop-in, here and in kv-storage-guide.md; the postfix iterator rewrite is the source change, and the duplicate-emplace and templated-bounds semantics are called out as matching upstream. The checklist no longer says direct db_* callers have nothing to do. README now says the contract is billed by default and that RAM follows the contract's payer argument. The ROA overview is referenced through wire-sysio#583 rather than a master URL that does not resolve yet. Depends on #113 for the emplace and lower_bound/upper_bound semantics this describes.
Third review round. Three findings were factual errors in text presented as checked, which is worse than an omission in a guide whose whole premise is that its claims were run. - The two `clio get table` invocations were CLI parse errors. `get table` declares two required positionals (account, table) with scope as `-S/--scope` (clio/main.cpp:2697-2699); both lines passed three. `roastate` is a `kv::global` and therefore unscoped, so the scope argument was wrong in concept as well. Added in round 2 to answer a review comment, never run. - The duplicate-key abort and the `name` bounds were stated as verified, but both arrive with wire-cdt#113 and are absent from any CDT built before it. The external dependency (wire-sysio#583) was already caveated; the in-repo one was not. Now says so, and records #113's receiver guards, which change behaviour for a port that opens a handle on another account. - "Before anyone can call it, a node owner must issue it a policy" is contradicted twenty lines later by the note that a `sysio.payer` caller reaches an unprovisioned contract fine. Now "before an ordinary caller". Also corrected, all verified against the tree rather than reasoned about: - `kv::global` + `_i` is broken at every name length, and the guide's advice to lengthen past 13 characters makes it worse. Below 14 the ABI carries two entries (`app_config` -> 38424 alongside a decoded-hash name `idrzzw4ktxljf` -> 21489, which is where the row actually lands); at 16 it fails to link outright with `table_id collision: 'app_config_table' and 'wdfp4hyupu.q2' both have table_id 42322`. The rule as written holds for `kv::table`. Documented with both rows rather than half-fixed; the in-tree `_i` globals are left alone because renaming them hits the second row. - The `kv::table` sample using `kv::table` included only `hash_id.hpp`, which pulls serialize.hpp and name.hpp and nothing else. - `table_id` is DJB2 over the eight big-endian bytes of the name's raw uint64, not over the string. - The byte-count row contradicted the row above it: Antelope's key is four 8-byte fields (32 B) and Wire's is a 2-byte table_id plus 16 (18 B). - `sed -i` is GNU-only and this project supports macOS. - Failed transactions are free objectively but still accrue subjective CPU against the first authorizer, which the same section says can throttle. - A KV iterator is copyable; the cost of duplicating its handle is why the postfix operators are deleted, not an inability to copy. - kv-multi-index.md called the shim a "drop-in replacement", the stronger form of the claim this PR already softened in kv-storage-guide.md. The `.gitignore` hunk is dropped from this PR. It was byte-identical to a strict subset of the one on #112 and #113 and conflicted with both; it now lands once, on #113.
The guide said a failure accrues subjective CPU against the first authorizer, which is true but omits the part that matters in a contract-pays guide: disable-subjective-payer-billing defaults to ON (producer_plugin.cpp:1478), so subjective_bill_failure skips the payer loop (subjective_billing.hpp:177) and runs only the authorizer loop (:184). Under contract-pays the payer is the contract and the first authorizer is the caller, so the caller absorbs the throttling and the contract does not -- which is what that default exists to do.
…caveat Third review round on the guide. Three findings, all of them cases where the text was confident and wrong. - The `-S <gen>` I added last round parses, but silently returns nothing for the generation a reader is most likely to try. CDT declares every scoped table's scope as a `name` and the chain honours that, trying `name(scope)` before falling back to an integer. `1`-`5` are valid name characters, so `-S 1` is read as the name "1" -- 576460752303423488 -- and the query returns no rows, which reads as "your issuer is not a node owner". Only generations containing a 0 or a 6-9 work, because name() rejects those characters and the fallback runs. Verified by computing the encoding for 0,1,2,5,6,9,10,11,21,100. The command now omits -S and reads network_gen off the rows. - The "Bytes per row" row was wrong on both sides and inverted the conclusion. Antelope's key_value_object is 108 + value with a 16-byte key, not a 32-byte one; Wire's kv_object is 112 + key + value. More importantly the guide implied Wire is cheaper per row. wire-sysio's own kv-ram-billing.md -- which this guide links -- gives 124 legacy vs 144 for sysio::multi_index (+16%) and 136 for kv::table (+10%) on a dense table. A ported contract needs MORE RAM, not less, and telling an author otherwise is how a provisioning policy comes up short. Says so now. - kv-multi-index.md gained a bullet asserting #113's guards as shipped fact, in the same commit that added the "arrives with #113" caveat to migrating-from-antelope.md. The caveat landed in one of the two files. It is in both now, and the "one known divergence" line -- there are three at this commit -- lists them. Also: - macOS sed guidance was insufficient and named the wrong package. `sed -i ''` alone does not help: BSD sed has neither \b nor \|, so \beosio\b matches a literal "beosiob" and renames nothing, silently, while the \(assert\|...\) group is a syntax error. gsed, from gnu-sed. - The db_* paragraph said "links but fails at deploy" while citing #112 in the same breath; after #112 removes them from the --allow-undefined-file list it is a link error. Both states described. - The table_id collision diagnostic comes from cdt-codegen's link-stage ABI finalize, not wasm-ld; `cdt-cpp -c` on the same file succeeds. - Upstream's bounds are member templates whose parameter cannot be deduced, not an overload set -- same outcome for &table::lower_bound, different reason, and the named static_cast escape hatch is Wire-only. - hash_id::max_length is 128 but nothing validates against it or against an alphabet; presented as a convention rather than a check. - kv-storage-guide.md still said "Drop-in EOSIO replacement", the stronger form of the claim this PR already softened elsewhere in the same file.
"broken at every length" overstated it. Reads and writes are correct -- the contract and the host both use the compile-time table_id, so the row is stored and fetched consistently, and hash_id_tests passes its integration suite (4/4) exercising exactly that path on a real chain. What is actually wrong is narrower: the ABI carries two table entries instead of one, so a get_table_rows by the readable name finds nothing. That is the same name-vs-hash mismatch the section already describes; the only kv::global-specific part is that lengthening the name past 13 characters -- the remedy for kv::table -- turns it into a table_id collision at link instead of fixing it.
Fourth review round. The "Budget more RAM, not less" paragraph was inserted INSIDE the comparison table, which terminated it: the `get_table_rows` row that followed became a lazy continuation of the paragraph and stopped rendering as a table row -- the very row the next sentence refers to. Moved out. More importantly the advice it carried was over-corrected. Last round fixed a claim that Wire is cheaper per row by asserting the opposite; both are wrong, because the direction depends on rows-per-scope. The `table_id_object` is billed per (code, scope, table), so a table with one row per scope -- a token balance, a per-user settings row, the most common Antelope shape -- sheds a whole 108-byte object per scope: wire-sysio's figures are 232 -> 144, a 38% SAVING. Only dense tables with few scopes pay more (124 -> 144, +16%), and across EOS mainnet the net is a 2-6% saving. The guide now gives the full four-row table and says to size from your own ratio. Also: - The BSD sed note claimed `\(assert\|...\)` is a syntax error. It is not: without REG_ENHANCED macOS sed treats `\|` as a literal `|`, so the group matches the literal text "assert|assert_message|..." and, like `\b`, simply renames nothing. Both halves fail silently, which is the part worth stating -- a reader told to expect an error concludes the script worked. - kv-multi-index.md's divergence list gained a `name`-bounds bullet last round that was itself an uncaveated #113 claim, in the same file where the guards bullet had just been caveated. Now carries the caveat, describes upstream's member template correctly, and gives the static_cast form that works on Wire. - Added the fourth real divergence, the trivially-copyable secondary-key static_assert (kv_multi_index.hpp:769), which the migration guide lists and its sibling did not. - Fixed a duplicated "the", a swallowed sentence, a bare #113 that does not autolink, a duplicated #112 mention, and a summary bullet that restated the _i 128-character limit as a rule after the body had just called it a convention.
Fifth review round. The `static_cast` the guide offers as the one working way to take a member pointer at the primary bounds does not compile: `const_iterator` is nested in the table and is not in scope at a use site, so it gives `unknown type name 'const_iterator'`. Verified, along with the qualified form that does work. The text was lifted from the header's own comment, where it sits inside the class and is correct in place. Fixed in both docs that carry it. "The compiler finds every one" was false for the postfix sweep. rbegin()/ rend() return std::reverse_iterator, whose postfix operators belong to the adaptor and are NOT deleted -- `for (auto rit = t.rbegin(); rit != t.rend(); rit++)` compiles clean and performs exactly the handle-duplicating copy the deletion exists to prevent. Verified both ways: rit++ compiles, it++ gives "overload resolution selected deleted operator '++'". Reverse loops need a manual grep, and the guide now says so. The examples/hash_id_example claim was wrong on two counts: its ABI has ZERO table entries, not the extra one (its structs sit at namespace scope, so abigen's discovery gate never sees them), and examples/ is not in the build graph or ctest, so "their integration tests pass" described a test that does not exist. Only hash_id_tests is affected, and its integration test does pass. The trivially-copyable rejection was over-stated: the static_assert lives in secondary_index_view, so it fires at get_index<...>(), not at declaration. A std::string-keyed secondary index compiles and can even be written through emplace. key_names/key_types were called Wire additions; they are original EOSIO table_def fields (abi_def.hpp:74-75). Only table_id and secondary_indexes are new -- what changed is that Wire populates the other two. The /usr/bin symlink rule was wrong: it is an enumerated allowlist, and eight cdt-* binutils aliases (cdt-ar, cdt-ranlib, cdt-nm, cdt-objcopy, cdt-objdump, cdt-readobj, cdt-readelf, cdt-strip) are deliberately excluded (InstallCDT.cmake:84-98). A reader invoking cdt-ar off PATH after a deb install gets command-not-found. Sibling drift, the recurring one: docs/kv-global.md's worked example is verbatim the pattern the guide calls a defect, and its long-name variant is the build-failure case; docs/kv-storage-guide.md's matrix still said `_i` was fine for kv::global. Both caveated, and kv-scoped-table.md's "drop-in replacement" is corrected the way the other two already were. Also: the #112 caveat now covers the cdt-abidiff limitations (fixed there) and distinguishes the two db_* declaration forms -- one carrying sysio_wasm_import emits an explicit import and links regardless of the allow-list, so it keeps failing at deploy rather than at link. Plus the exact deleted-operator diagnostic, the full collision message, protobuf's 1.3, the double/long-double sign-flip, per-action rather than per-transaction first authorizer, nodeowners being scoped-by-generation/keyed-by-owner, and the absence of an assert_sha3.
Both PRs the docs referenced as pending are merged, and one of them made a sentence here false. Every claim below was re-verified against a CDT built from merged master, not just reworded. The "unchanged host functions" list promised "every privileged.h setter". #112 removed set_kv_parameters_packed, which was one. The list now names the setters that remain, and the two removals get rows in the "Removed on Wire" table with the diagnostic each produces -- they differ, and the difference is useful when porting: <sysio/security_group.h> is gone outright ("file not found"), while privileged.h is still there and set_kv_parameters_packed is an undeclared identifier in it. Both confirmed by compiling; set_privileged from the same header still builds. The ABI section told the reader not to reach for cdt-abidiff, on the strength of limitations #112 fixed. Inverted: it now lists the sections the tool compares -- version as the full string, structs, types, actions, tables with the full metadata, clauses, enums, protobuf_types, variants, action_results and error_messages -- and keeps the old-toolchain caveat and the jq fallback for anyone on an older CDT. The legacy-database and multi_index sections described #112 and #113 as forthcoming. They describe the current toolchain now, with the pre-merge behaviour as the caveat. Verified against merged master: `it++` is still rejected with "overload resolution selected deleted operator '++'", a hand declared db_store_i64 now fails with "wasm-ld: undefined symbol: db_store_i64", and the documented static_cast escape hatch plus lower_bound on name, uint64_t and {42} all compile. Both docs also now say the #113 receiver guard reaches sysio::singleton: get_or_create, set and remove mutate through the kv_multi_index it holds, so a singleton handle on another account's code is read-only like a table handle. The kv-multi-index singleton section also said singleton "is backed by" kv_multi_index, which reads as inheritance; it holds one.
Each correction below was checked against a CDT built from master before it was written. migrating-from-antelope.md Floating secondary keys do not sort as they did. The legacy idx_double / idx_long_double indices rejected NaN and folded -0.0 onto +0.0; Wire's encoder is a pure bit transform, so it admits NaN and sends the signed zeros to distinct ranges (-0.0 below +0.0). Narrowed to the integer types, with the edge cases spelled out. The 2-6% RAM figure is an estimate that excludes xsat, whose 345M idx256 entries dominate the population. Both populations are now given. assert_sha3 exists, and so do keccak / assert_keccak -- all four are in crypto_ext.hpp and compile. The claim that sha3 has no assert_ form was simply wrong. Only three tools carry a sysio-* alias: sysio-pp, sysio-wast2wasm, sysio-wasm2wast. The text implied all eleven entry points did. The payer checklist item covered emplace and modify only. singleton::set and get_or_create forward their payer into kv_multi_index::emplace/modify and hit the same rejection, so they are named. Two of my own from the previous commit: the checklist still told readers to avoid cdt-abidiff for table metadata, contradicting the section 400 lines earlier that this branch had already fixed; and the section's list of compared sections omitted abi_extensions, which diff_opaque_section handles. kv-multi-index.md The postfix sweep is not fully compiler-caught: rbegin/rend hand back a std::reverse_iterator whose postfix operators are not deleted. The main guide said so; this page promised the opposite. The real bound overload break is an explicit template argument -- t.template lower_bound<uint64_t>(k) is valid upstream and is "error: 'lower_bound' following the 'template' keyword does not refer to a template" here -- plus the dual-convertible ambiguity. The bare member-address case does not compile upstream either, so it was never Wire-specific. Upstream does not accept arbitrary serializable secondary keys: its backend is exactly the five db_idx* families, the same fixed set. Only the diagnosis differs. Removed the false contrast. kv-global.md The _i failure is a kv::global ABI bug, fixed by #115, not a property of _i -- which works and is tested on kv::table. Scoped the warning to a pre-#115 toolchain and stopped implying _i is generally unsuitable. The name-alphabet rule now says the 13th position takes only .12345a-j, being 4 bits not 5. The complete example emitted NO ABI table entry: a namespace-scope struct with only [[sysio::table]] is not reachable from the contract, and abigen skips it. Moved it inside the contract class -- it now emits ('appconfig', 46121) -- and the ABI note says what reachability requires. The shipped examples/kv_global_example has the same defect and is not fixed here. kv-scoped-table.md Not source-compatible with multi_index: the template goes <Name,T,...> -> <Name,K,V,...>, the row splits, primary-key access changes and the payer moves. Called a migration target, which its own worked conversion shows it to be.
7ed8f26 to
64b40a9
Compare
The guide told readers to use _i only above 13 characters, because a shorter annotated name was routed through string_to_name by the ABI generator while the literal hashed -- user_table ran at 61956 and was advertised as 3509. wire-cdt#115 makes the template-derived table_id authoritative, so both lengths now agree. Restated as a pre-#115 caveat, matching how this guide already handles #112 and #113, and the "has to be renamed, or lengthened past 13 characters, until abigen is fixed" advice is dropped -- _i is now the answer for a name outside the _n alphabet at any length. The _n alphabet note also gains the 13th-position restriction (.12345a-j), which is 4 bits rather than 5.
Summary
Adds
docs/migrating-from-antelope.md— a guide for developers arriving with a working contract from EOS, Telos, WAX, Jungle or another Antelope-family chain. Documentation only; no code changes.It is organised as the port actually goes: install and build something first, then the mechanical renames, then storage, then the resource model, then the host-function diff.
What it covers
-devpackages, plus CMake, a build tool andjq, which the guide's own commands need), theeosio-*→cdt-*/cleos→cliomapping,cdt-init,add_contract, native testing, getting an account,sysio.roa::addpolicy, and deploy. The tarball'sbin/holds the unprefixedclang/lld/llvm-*, so putting it onPATHshadows the distro toolchain — that is called out, along with thecdt-cppname collision with AntelopeIO CDT 3.0+.eosio→sysiotable, asedfirst pass that explicitly covers theeosio_assert/eosio_exitC API a\beosio\bpattern silently skips, and a caution about string literals. The ABI version becomessysio::abi/1.2.table_idandsecondary_indexesare the Wire additions totable_def;index_type,key_namesandkey_typesare original EOSIO fields — what changed is that Wire populates them to describe the key layout.db_*_i64anddb_idx*are gone from CDT and the chain.sysio::multi_indexis a compatibility shim — table declarations carry over, but the postfixit++/it--operators are deleted and need a mechanical sweep. Secondary keys must bestd::is_trivially_copyable, not merely serializable.get_table_rowsnow returns{key, value}. Use_ionly above 13 characters, and note that_nis not a universal fallback — its alphabet is.12345a-z, so a name containing_is a compile error. Reviewing that rule turned up a toolchain defect, now fixed in fix(abigen): keep the annotated table name for a_i-named table #115 rather than documented:abigennamed a table by decoding its raw template parameter, which is a hash for_i, sokv::global,multi_indexandkv_multi_indexemitted a garbage twin beside the annotated name — two mismatched entries under 13 characters, atable_id collisionat or above it.kv::tablewas never affected, because its code path already preferred the annotation. With fix(abigen): keep the annotated table name for a_i-named table #115 merged,_iworks at any length on every table kind, andkv-global.mdcarries a caveat for older toolchains instead of a prohibition.emplace(user, …)→emplace(get_self(), …)decision, whysysio.payerat index 0 welds RAM billing to CPU/NET (and the advanced@sysio.codedelegation that separates them), and that CPU is billed per top-level action across the whole call tree while NET is fixed by what reaches the wire.kv_*,get_ram_usage,get_permission_lower_bound,blake2b_256), removed (db_*_i64, the 50db_idx*,send_deferred/cancel_deferred,get_permission_last_used/get_account_creation_time), and the unchanged surface.Verification
Every claim was checked against the code rather than assumed, and doing so corrected several of my own errors — including two rounds of review findings that were all accurate.
Notable things the checks changed:
get_block_numis standard Antelope, so it is not a Wire addition;blake2b_256is not in Antelope'scrypto_ext.h, so it is one.addpolicyexample had a C++/ABI camelCase-vs-snake_case note that described a divergence that does not exist — I had readwire-system-contracts' copy ofsysio.roa.hpp, abandoned on a 2025 branch, rather than the authoritativewire-sysio/contractscopy._ishort-nametable_idmismatch is real and reproduced independently:user_tablehashes to 61956 at runtime and 3509 in the ABI, becauseabigenroutes annotated names of ≤13 characters throughstring_to_namewhile_ialways hashes.overhead / actions + 1— integer division then an unconditional+1, which over-bills by up to a byte where the division is exact.Related
emplaceguard and the receiver guards, and adds anameoverload tolower_bound/upper_bound(an earlier revision of fix(kv): reject duplicate primary keys in multi_index::emplace #113 templated them, then replaced that with a converting proxy; both were reverted in favour of the two concrete overloads, so fix(kv): reject duplicate primary keys in multi_index::emplace #113's own history is misleading on this point if read at an intermediate commit). The storage section describes the post-fix(kv): reject duplicate primary keys in multi_index::emplace #113 semantics, so this should merge after it. Without fix(kv): reject duplicate primary keys in multi_index::emplace #113 the same section would have to document anemplacethat silently upserts and strands secondary-index entries.masterpath does not resolve yet, and the guide says so rather than linking a 404.Corrections worth calling out
Two of the guide's claims were confidently wrong and are fixed here, because both would have misled the reader in the direction that costs money:
table_id_objectis billed per(code, scope, table), so a table with one row per scope — a token balance, a per-user settings row — sheds a whole 108-byte object and comes out 38–41% cheaper, while a dense table with few scopes pays 10–16% more. Across EOS mainnet the net is a 2–6% saving. The guide now gives all four rows from wire-sysio'skv-ram-billing.mdand says to size from your own ratio rather than assuming either way.clio get table ... -S <gen>. Scoped-table scopes are declared asname, and the chain triesname(scope)before falling back to an integer.1-5are valid name characters, so-S 1is read as the name"1"(576460752303423488) and the query returns no rows — which reads as "your issuer is not a node owner". The command now omits-S.Not in this PR
The
.gitignorehunk was dropped — it was a strict subset of the identical one on #112 and #113 and conflicted with both. It landed on #113, and this branch now takes master's version.