Introduce automatic ABI maintenance mechanism (1/2; prepare) (#10335)
* Introduce automatic ABI maintenance mechanism * Compile fix... * Docs fix... * Programs compilation fix... * Simplify source credit Co-authored-by: Michael Vines <mvines@gmail.com> * Cargo.lock... Co-authored-by: Michael Vines <mvines@gmail.com>
This commit is contained in:
@ -101,6 +101,7 @@
|
||||
* [Snapshot Verification](implemented-proposals/snapshot-verification.md)
|
||||
* [Cross-Program Invocation](implemented-proposals/cross-program-invocation.md)
|
||||
* [Program Derived Addresses](implemented-proposals/program-derived-addresses.md)
|
||||
* [ABI Management](implemented-proposals/abi-management.md)
|
||||
* [Accepted Design Proposals](proposals/README.md)
|
||||
* [Ledger Replication](proposals/ledger-replication-to-implement.md)
|
||||
* [Optimistic Confirmation and Slashing](proposals/optimistic-confirmation-and-slashing.md)
|
||||
@ -114,6 +115,5 @@
|
||||
* [Slashing](proposals/slashing.md)
|
||||
* [Tick Verification](proposals/tick-verification.md)
|
||||
* [Block Confirmation](proposals/block-confirmation.md)
|
||||
* [ABI Management](proposals/abi-management.md)
|
||||
* [Rust Clients](proposals/rust-clients.md)
|
||||
* [Optimistic Confirmation](proposals/optimistic_confirmation.md)
|
||||
|
@ -55,7 +55,7 @@ fields.
|
||||
# Example
|
||||
|
||||
```patch
|
||||
+#[frozen_abi(digest="1c6a53e9")]
|
||||
+#[frozen_abi(digest="eXSMM7b89VY72V...")]
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Vote {
|
||||
/// A stack of votes starting with the oldest vote
|
||||
@ -73,16 +73,85 @@ digest from the assertion test error message.
|
||||
|
||||
In general, once we add `frozen_abi` and its change is published in the stable
|
||||
release channel, its digest should never change. If such a change is needed, we
|
||||
should opt for defining a new struct like `FooV1`. And special release flow like
|
||||
hard forks should be approached.
|
||||
should opt for defining a new `struct` like `FooV1`. And special release flow
|
||||
like hard forks should be approached.
|
||||
|
||||
# Implementation remarks
|
||||
|
||||
We use some degree of macro machinery to automatically generate unit tests
|
||||
and calculate a digest from ABI items. This is doable by clever use of
|
||||
`serde::Serialize` (`[1]`) and `any::typename` (`[2]`). For a precedent for similar
|
||||
`serde::Serialize` (`[1]`) and `any::type_name` (`[2]`). For a precedent for similar
|
||||
implementation, `ink` from the Parity Technologies `[3]` could be informational.
|
||||
|
||||
# Implementation details
|
||||
|
||||
The implementation's goal is to detect unintended ABI changes automatically as
|
||||
much as possible. To that end, the digest of structural ABI information is
|
||||
calculated with best-effort accuracy and stability.
|
||||
|
||||
When the ABI digest check is run, it dynamically computes an ABI digest by
|
||||
recursively digesting the ABI of fields of the ABI item, by re-using the
|
||||
`serde`'s serialization functionality, proc macro and generic specialization.
|
||||
And then, the check `assert!`s that its finalized digest value is identical as
|
||||
what is specified in the `frozen_abi` attribute.
|
||||
|
||||
To realize that, it creates an example instance of the type and a custom
|
||||
`Serializer` instance for `serde` to recursively traverse its fields as if
|
||||
serializing the example for real. This traversing must be done via `serde` to
|
||||
really capture what kinds of data actually would be serialized by `serde`, even
|
||||
considering custom non-`derive`d `Serialize` trait implementations.
|
||||
|
||||
# The ABI digesting process
|
||||
|
||||
This part is a bit complex. There is three inter-depending parts: `AbiExample`,
|
||||
`AbiDigester` and `AbiEnumVisitor`.
|
||||
|
||||
First, the generated test creates an example instance of the digested type with
|
||||
a trait called `AbiExample`, which should be implemented for all of digested
|
||||
types like the `Serialize` and return `Self` like the `Default` trait. Usually,
|
||||
it's provided via generic trait specialization for most of common types. Also
|
||||
it is possible to `derive` for `struct` and `enum` and can be hand-written if
|
||||
needed.
|
||||
|
||||
The custom `Serializer` is called `AbiDigester`. And when it's called by `serde`
|
||||
to serialize some data, it recursively collects ABI information as much as
|
||||
possible. `AbiDigester`'s internal state for the ABI digest is updated
|
||||
differentially depending on the type of data. This logic is specifically
|
||||
redirected via with a trait called `AbiEnumVisitor` for each `enum` type. As the
|
||||
name suggests, there is no need to implement `AbiEnumVisitor` for other types.
|
||||
|
||||
To summarize this interplay, `serde` handles the recursive serialization control
|
||||
flow in tandem with `AbiDigester`. The initial entry point in tests and child
|
||||
`AbiDigester`s use `AbiExample` recursively to create an example object
|
||||
hierarchal graph. And `AbiDigester` uses `AbiEnumVisitor` to inquiry the actual
|
||||
ABI information using the constructed sample.
|
||||
|
||||
`Default` isn't enough for `AbiExample`. Various collection's `::default()` is
|
||||
empty, yet, we want to digest them with actual items. And, ABI digesting can't
|
||||
be realized only with `AbiEnumVisitor`. `AbiExample` is required because an
|
||||
actual instance of type is needed to actually traverse the data via `serde`.
|
||||
|
||||
On the other hand, ABI digesting can't be done only with `AbiExample`, either.
|
||||
`AbiEnumVisitor` is required because all variants of an `enum` cannot be
|
||||
traversed just with a single variant of it as a ABI example.
|
||||
|
||||
Digestable information:
|
||||
|
||||
- rust's type name
|
||||
- `serde`'s data type name
|
||||
- all fields in `struct`
|
||||
- all variants in `enum`
|
||||
- `struct`: normal(`struct {...}`) and tuple-style (`struct(...)`)
|
||||
- `enum`: normal variants and `struct`- and `tuple`- styles.
|
||||
- attributes: `serde(serialize_with=...)` and `serde(skip)`
|
||||
|
||||
Not digestable information:
|
||||
|
||||
- Any custom serialize code path not touched by the sample provided by
|
||||
`AbiExample`. (technically not possible)
|
||||
- generics (must be a concrete type; use `frozen_abi` on concrete type
|
||||
aliases)
|
||||
|
||||
# References
|
||||
|
||||
1. [(De)Serialization with type info · Issue #1095 · serde-rs/serde](https://github.com/serde-rs/serde/issues/1095#issuecomment-345483479)
|
Reference in New Issue
Block a user