checkSign rejects every pseudo-account when BatchV1_1 is enabled, but
emitted transactions need BatchV1_1 to skip the signature check. So no
contract could emit anything. Allow a contract pseudo-account on inner
transactions, and make emitBuiltTxn and emitTxn reject a transaction
whose Account is not the running contract's account. Without that
check, a contract could emit an unsigned transaction from any account.
The two groups share one flat ABI table in one `host_lib` namespace, and
the only thing separating them is that the escrow host leaves the contract
methods at their `Unimplemented` default. That separates them too late,
and in one direction only.
An escrow whose bytecode imports `emit_built_txn` passes screening, so
`EscrowCreate` stores it and the escrow fails at finish with
`tecINTERNAL`, because `Unimplemented` is a fatal fault rather than a code
a contract reads. A contract calling `set_data` does not fail at all: the
method is inherited, only `EscrowFinish::doApply` drains it, so the write
returns a byte count and is discarded.
Both need screening to know which subset of the ABI the host being built
for actually serves. `docs/wasm-amendment-gating.md` already designs that
transport for amendments; this wants the same one keyed on which host is
running.
Both emit host functions rebuilt an `STTx`, and both were caught by the
outer guard when the rebuild threw, so a contract that got its own
transaction wrong was answered `InternalFatal`: the run stopped and the
transaction reported `tecINTERNAL`. Either was reachable from any guest.
`emit_built_txn` constructed the transaction before its try, so a field
the type requires but the contract never set threw there. The
construction moves inside the guard and answers `SubmitTxnFailure`, which
the contract can act on. It also copies rather than moves out of the
builder, so a contract told its transaction is malformed can supply what
was missing and emit the same index again.
`emit_txn` could not emit anything a contract had not already marked
`tfInnerBatchTxn`. Setting the flag meant rebuilding the transaction from
its fields, and that re-runs the format check, which rejects the defaulted
`sfPaths` an ordinary Payment carries harmlessly. The flag now goes on a
copy made through `STTx`'s own copy constructor, which does not re-check,
and through `setFieldU32` rather than `setFlag`: a transaction built
against a template holds `sfFlags` as a placeholder until something gives
it a value, and `setFlag` can only reach one that already has it.
The tests that pinned the old behavior now pin the new.
The two byte-order conventions are the part worth writing down: a
parameter answers little-endian integers, a data read answers the
canonical big-endian serialization, and the TER an emit produces is four
little-endian bytes in an output region rather than the call's result.
Adds the four marshalling shapes the contract functions bring to the e2e
table, and the fixtures that stand a contract host up.
`Contract_test.cpp` loaded its contract bytecode from a hardcoded path
into a developer's home directory, so on every other machine the loader
returned an empty string and the transaction was refused as malformed.
Those testcases have been failing wherever that directory does not exist,
which is everywhere. The four Rust guest crates they were meant to load
were never compiled by anything, and the script that once embedded such
fixtures is gone upstream.
What they covered now lives in the layered wasm tests: the host functions
themselves in `host_functions/`, the wire in `host_calls/`, and the
marshalling shapes end to end in `e2e/`, against WAT the suite assembles
itself. `ContractHostFuncImpl_test.cpp` goes with them; seven of its eight
testcases were already commented out.
What remains in `Contract_test.cpp` is the transactor surface the new
tests do not reach: preflight, preclaim and doApply for create, modify and
delete, driven from inline bytecode.
One of those was also failing, and for a reason of its own: the
"Functions array too large" case declared thirteen functions against a
limit of thirty-two, so the array was never too large and the transaction
fell through to the fee check. It now counts from the limit.
Three shapes the contract host functions add to the e2e table, each one a
convention a guest and a host could agree about separately and still
disagree about in practice.
- A typed value written by the guest and read back without its type byte,
answered out of the cache by a write in the same run, for an account
whose data object is not on the ledger at all.
- A scalar index between two regions. Writing element 0 and element 1 and
reading them apart is what shows an index taken from the wrong place;
reading back the only element written would not.
- The builder index, which is the only contract host state that outlives
one call, together with the TER crossing as bytes in a region rather
than as the call's result.
- An `STJson` object the guest serialized, a different parser from the one
a stored value goes through.
`ContractVmTest` takes the host rather than building one, because what
these tests assert is what the contract left behind, which only the
caller's `ContractHost` can be asked about afterwards.
Completes the `host_calls/` layer: a mock host behind the real VM, with
each module hand-writing its own imports, literal regions and literal
lengths, so what is tested is the wire rather than whatever an SDK sends.
The order of two same-shaped arguments is the thing worth asserting here,
because nothing downstream would notice them swapped: a nested read takes
the outer key first, and an array read takes the key before the index
while the host takes them the other way round. Each is asserted against a
value the other could not be.
What the shim refuses before the host is reached, now stated: an account
region that is not twenty bytes, a key that is not UTF-8, a negative
index, a transaction type that does not fit in sixteen bits, a field code
the protocol does not name, bytes that are not a transaction, bytes that
are not an object, and an empty value region.
`SliceIs` is added beside `BytesAre` for data that is not text. `BytesAre`
compares against a string and so stops at the first NUL, which most
serialized fields contain — an amount matched as "@" and passed.
Completes the `host_functions/` layer: reads, nested objects, arrays,
nested arrays, and the two emit paths, each against a real ledger.
What the shapes turn out to mean, now asserted rather than assumed:
- An account has one data object, and the first write decides whether it
is a map of keys or a list of elements. Mixing them is refused rather
than silently replacing one with the other.
- The two string arguments of a nested write are the outer key then the
inner one. Reading under the swapped pair finds nothing, which is what
a contract that got the order wrong would see.
- Two arrays under two keys are two arrays.
- A stored integer reads back big-endian, the opposite of the
little-endian a parameter answers with.
Two more defects are pinned rather than fixed, both the same shape as the
one in the previous commit: reconstructing an `STTx` re-runs its format
check, and the throw lands outside the guard that would have answered
`SubmitTxnFailure`. `emit_txn` cannot emit a transaction that does not
already carry `tfInnerBatchTxn`, because setting the flag means rebuilding
it, and an ordinary Payment's defaulted `sfPaths` is rejected on the way
back in. The contract is told `InternalFatal` and the run stops.
`ContractLedger` now gives each host a transaction of its own; two
identical ones share an id, which a ledger refuses to record twice.
One `host_functions/` suite per function, in upstream's layering: a real
`ContractHostFunctionsImpl` over a real ledger, no VM.
What they pin beyond the happy path:
- A parameter comes back little-endian, unlike the big-endian canonical
serialization a data read answers. Both conventions are in the ABI, so
both are asserted.
- The instance's parameters and the call's are separate tables.
- A data write is visible to a later read in the same run, through the
cache, before anything reaches the ledger; and what is finalized is
visible to a host that has no cache at all.
- The data object's owner pays its reserve, so an owner with nothing
spare cannot be written to.
- An account field carries its length before its id. The bare twenty
bytes are not a field.
- A payment the contract cannot afford is reported as its `tec` and still
queued: a fee-claiming transaction belongs on the ledger. That code is
negative, which is what the TER output region exists for.
`ATransactionMissingARequiredFieldStopsTheRun` records a defect rather
than a design: a built transaction that fails its format check throws
where `STTx` is constructed, outside the guard that would have answered
`SubmitTxnFailure`, so the contract's own mistake reads as `InternalFatal`
and the run reports `tecINTERNAL`. Left as it is here and worth a fix of
its own.
`ContractLedger` is the contract counterpart of `WasmLedger`: a real
genesis ledger with a real `ContractHostFunctionsImpl` over it, built the
way `ContractCall::doApply` builds one. It carries no test framework, for
the reason `WasmLedger` carries none — the benchmarks need a ledger and a
host, not GTest's lifecycle — so it joins `xrpl.testkit.wasm`.
`ContractHost` owns the four things the host borrows, in the order it
needs them, and its `finalize()` does what the transactor does once a
contract returns: write the data cache and the events to the ledger, then
apply. Applying alone is not enough, because `ApplyContext` reaches a
sandbox rather than the ledger under it, so `finalize()` commits that too.
`ContractHostFixture` adds the framework and the value constructors a
data test compares against, including `valueWire`, which produces the
bytes a guest writes for a value: the inverse of what `HostContext`
decodes.
`WasmLedger::tracingJournal` is extracted so a derived fixture can build
a tracing host over the same sink.
The 15 smart-contract host functions were registered through
`HostFuncWrapper.cpp` and `createWasmImport`, both of which upstream
deleted. They are now declared where every other host function is: one
`host_functions!` entry each in `crates/xrpl-host-functions`, which
generates the trait, the spec table and the wasmi registration, with a
body in the engine, a crossing in the cxx bridge, and a `HostContext`
method lowering it onto the wire.
They share the `host_lib` namespace with the escrow functions, whose host
leaves them `Unimplemented`. Wire order and gas are unchanged, so a guest
built against the old ABI still links, with two exceptions.
`emit_built_txn` and `emit_txn` gain an output region and write the TER
there as four little-endian bytes, returning the length. Their old
signature returned the TER as the call's result, which the engine reads
as a `HostError` when negative: every `tem`, `tef`, `ter` and `tel` code
would have stopped the run instead of reaching the contract.
`HostContext` decodes what the old wrappers decoded: the account region,
the typed value a `set_data_*` call carries, the serialized transaction
and the serialized event. A guest that writes those bytes wrongly gets
`InvalidParams`, so the deserializers' exceptions are caught at the call
rather than by `guarded`, which would report a node fault.
Every Rust site that enumerates the ABI is extended: the two literal
tables in `generated_abi.rs`, the import list and arity check in
`preflight.rs`, the per-function wasm in `budgets.rs`, and the three fake
hosts. `MockHostFunctions` gains the 15 methods and the matchers a
contract expectation needs.
Two `host_calls` tests cover the shapes that are new here: a typed value
decoded on its way to the host, and a TER crossing as bytes, including
the negative code that motivated the change.
Upstream rewrote the wasm VM and host-function system (Rust `crates/`
bridged via cxx), collapsed `transactions.macro` onto `TxSettings`, and
moved invariant running from `ApplyContext` to `Transactor`. This merge
resolves those conflicts and the breaks that carried no conflict marker.
Conflict resolutions of note:
- transactions.macro: took upstream's 5-argument `TxSettings` form and
re-expressed our `emitable` column as `TxSettings::emittance`, a new
scoped `Emittance` enum in TxSettings.h. `Emitable.cpp` and the
transaction code generator read the new member.
- sfields.macro: upstream claimed UINT32 75-80, so `sfParameterFlag`
moves from 80 to 86. The amendment is not live, so no wire break.
- HostFunc.h: took upstream's version, which drops `floatRoot`, and
re-added the 15 contract virtuals. `setDataNestedObjectField`'s
parameters are renamed to `(account, key, nestedKey, value)` to match
the implementation; the wire order is unchanged.
- WasmCommon.h: `SubmitTxnFailure` (-21) and `InvalidState` (-22) join
upstream's enum and the Rust `host_errors!` table. `Success` is gone;
the helpers that compared against it now return `expected<void, ...>`.
- Transactor.cpp: kept the emitted-transaction pass, now using
`checkInvariants(result, fee, InvariantScope::ProtocolOnly)`.
Breaks with no conflict marker:
- `Emitable.cpp` used the 8-argument TRANSACTION macro.
- `STTx::getSeqValue` is gone; use `getSeqProxy().value()`.
- `NetworkOPsImp::subLock_` is now `streamLock_`, held with `scoped_lock`.
- The `LedgerEntryHelpers` namespace is now `ledger_entry_helpers`.
- `keylet::vault` takes a `SeqProxy`. The three contract `ledger_entry`
parsers were copy-pasted from the vault one and returned vault keylets;
they now build contract, contract source and contract data keylets from
their own fields. `contract_hash` is added to jss.
The 15 contract host functions still register through the deleted C++
wrapper layer, so contract bytecode does not run yet. Porting them to the
Rust-declared ABI is the next commit.