Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a41f1b0
feat(platform): add contract-code size limits and block consensus par…
DCG-Claude Sep 12, 2026
526160c
feat(dpp): detect the state transition family from the wire prefix an…
DCG-Claude Sep 12, 2026
23612b3
feat(drive-abci): decode contract-code envelopes under the family cap…
DCG-Claude Sep 12, 2026
087d35f
feat(rs-dapi): admit contract-code state transitions up to the family…
DCG-Claude Sep 12, 2026
5718bd9
test(drive-abci): pin the block parameter push across a protocol upgr…
DCG-Claude Sep 12, 2026
2573bbe
feat(dashmate): size the Tenderdash transaction, RPC body and bandwid…
DCG-Claude Sep 12, 2026
c860edf
docs(platform): describe the per-family state transition caps, decode…
DCG-Claude Sep 12, 2026
be45a42
fix(rs-dapi): raise the Tenderdash WebSocket frame limit for large ev…
DCG-Claude Sep 12, 2026
1cf07ab
test(drive-abci): retry the activation block itself when pinning the …
DCG-Claude Sep 12, 2026
105204f
fix(dpp): report the bounded decode budget in kilobytes
DCG-Claude Sep 15, 2026
65e965f
fix(rs-dapi): keep the large request allowance specific to the transa…
DCG-Claude Sep 16, 2026
894f8fb
fix(rs-dapi): scope the body limit to Platform and check the declared…
DCG-Claude Sep 16, 2026
298aaad
fix(dpp): pick the contract-code capable generation index per contrac…
DCG-Claude Sep 29, 2026
95dd58d
test(dashmate): derive the recorded config format in the image migrat…
DCG-Claude Sep 29, 2026
42275ae
fix(rs-dapi): check the header of every gRPC message in the body limi…
DCG-Claude Sep 29, 2026
fd95459
docs(platform): name the real contract-code generations and decoder i…
DCG-Claude Sep 29, 2026
3fea083
fix(rs-dapi): skip the orchard decode for contract-code envelopes and…
DCG-Claude Sep 29, 2026
a84cf41
test(dpp): pin the discriminant of the family size error
DCG-Claude Sep 29, 2026
141b4a2
docs(platform): name decoder v2 in the limits pin and the real v0 pro…
DCG-Claude Sep 29, 2026
b207f1f
fix(rs-dapi): bound concurrent large broadcast bodies before tonic re…
DCG-Claude Sep 29, 2026
ce89c7d
fix(rs-dapi): cap large broadcast bodies per source
DCG-Claude Sep 30, 2026
17e68c3
docs(dpp): name the real callers of the bounded decode
DCG-Claude Sep 30, 2026
922b621
fix(rs-dapi): hold the large-body slot until the broadcast call ends
DCG-Claude Sep 30, 2026
6ffc8a9
test(dpp): cover every contract transition generation exhaustively
DCG-Claude Sep 30, 2026
199c5fb
test(rs-dapi): bound every wait of the held-slot regression
DCG-Claude Sep 30, 2026
6ee3808
test(dpp): pin leftover-byte rejection under both decode budgets
DCG-Claude Sep 30, 2026
ffae16f
docs(platform): count SYSTEM_LIMITS_V5 in the feature versions chapter
DCG-Claude Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion book/src/error-handling/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Error codes are organized into ranges that correspond to error categories and su
| 10400-10424 | Documents | `DataContractNotPresentError` (10400), `DuplicateDocumentTransitionsWithIdsError` (10401), `DocumentPropertyNotDistinctError` (10419), `InvalidEncryptedPropertyShapeError` (10420), `DocumentPropertyMaxBytesExceededError` (10421), `DocumentPropertyConstraintViolatedError` (10422), `DocumentPropertyNotGeneratedError` (10424) |
| 10450-10460 | Tokens | `InvalidTokenIdError` (10450), `TokenTransferToOurselfError` (10456) |
| 10500-10535 | Identity | `DuplicatedIdentityPublicKeyBasicError` (10500), `InvalidIdentityPublicKeyDataError` (10511) |
| 10600-10603 | State Transition | `InvalidStateTransitionTypeError` (10600), `StateTransitionMaxSizeExceededError` (10602) |
| 10600-10604 | State Transition | `InvalidStateTransitionTypeError` (10600), `StateTransitionMaxSizeExceededError` (10602), `StateTransitionFamilyMaxSizeExceededError` (10604, a contract-code capable contract transition above its own family cap) |
| 10700-10700 | General | `OverflowError` (10700) |
| 10800-10818 | Address | `TransitionOverMaxInputsError` (10800), `WithdrawalBelowMinAmountError` (10818) |
| 10819-10827 | Shielded | `ShieldedNoActionsError` (10819), `ShieldedTooManyActionsError` (10825), `ShieldedImplicitFeeCapExceededError` (10826), `ShieldedInvalidDenominationError` (10827 — `IdentityCreateFromShieldedPool` exit amount not a member of the versioned denomination set) |
Expand Down
53 changes: 53 additions & 0 deletions book/src/state-transitions/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,59 @@ pub fn deserialize_from_bytes_in_version(
This ensures that a transition type introduced in protocol version 9 cannot be
submitted to a node running protocol version 8.

### Size caps and decode budgets per family

Two different numbers bound a serialized state transition, and they are
enforced at different points:

- **The wire cap** is a plain comparison on the raw byte length, made before
any decoding. Every ordinary family shares
`SystemLimits::max_state_transition_size` (20 KiB). From protocol version 17
the contract create and update transitions, in the generation that can carry
a code bundle, get their own cap,
`SystemLimits::max_contract_code_state_transition_size` (32 MiB,
provisional).
- **The decode budget** is bincode's `with_limit`. It counts the bytes read
*and* the allocation claims the `Value` decoder makes for every map and
array while decoding a contract schema, so it has to sit well above the wire
cap. The contract-code envelopes decode under
`SystemLimits::max_contract_code_state_transition_decode_budget` (64 MiB,
provisional). The ordinary families keep the decode they shipped with: the
`limit = 100000` on the enum sits in a second `platform_serialize` attribute
the derive never reads, so `deserialize_from_bytes_untrusted_exact` applies
no bincode budget and the wire cap is the only bound on them. That decode is
not changed retroactively; the contract-code envelopes are the first family
decoded under an explicit budget. Both budgets run the untrusted bincode
decoder and refuse bytes left over after the transition.

The family is read from the wire prefix before anything is decoded.
`StateTransition::peek_envelope_kind` looks at the first ten bytes at most
(the outer variant index of `StateTransition`, then the inner variant index
of the contract transition enum) and returns `Ordinary` or
`ContractCodeCapable { family }`. The code-bearing generation is chosen per
family as the one after its newest existing generation: create `V2` (the
contract-group `V1` from protocol version 14 stays ordinary) and update
`V1`. Every generation that exists today, and an unknown or truncated prefix,
is `Ordinary`, so it is bounded by the small cap and fails decode as it
always did. `family_max_size` and `family_decode_budget` map the kind onto the
tables of the active version, and
`deserialize_from_bytes_in_version_bounded` decodes under that budget and
reports a not-yet-active variant as `StateTransitionNotActiveError` (an
unpaid consensus rejection) rather than as a protocol error. The block
decoder (`decode_raw_state_transitions` v2), the `getProofs` query (v1) and
the DAPI broadcast pre-filter all go through these helpers so every ingress
path applies the same cap; the frozen v0 and v1 decoders keep calling
`deserialize_from_bytes_untrusted_in_version` and
`deserialize_from_bytes_untrusted_exact_in_version` unchanged.

Because the block byte limit is a Tenderdash consensus parameter, raising it
for the large envelopes is protocol state as well:
`ConsensusVersions::block_max_bytes` and `block_max_gas` are pushed by
`consensus_params_update` v2 from both proposal paths at the activation
boundary, so every validator adopts them at the same height. The node-local
Tenderdash mempool (`max-tx-bytes`) and RPC (`max-body-bytes`) limits are
dashmate options sized to match.

## The Full Journey

Here is the end-to-end lifecycle of a state transition, from a client's perspective
Expand Down
26 changes: 22 additions & 4 deletions book/src/versioning/feature-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ pub struct SystemLimits {
}
```

There are four `SYSTEM_LIMITS_V*` constants, one for each protocol version at
There are five `SYSTEM_LIMITS_V*` constants, one for each protocol version at
which a limit changed. The `Option` fields show the idiom for a parameter that
did not exist before some version: `None` in the tables of the versions that
predate the rule, `Some(value)` from the version that introduced it. It is the
Expand Down Expand Up @@ -406,8 +406,26 @@ every time the number moves. A new method version is warranted only when the
*logic* changes. `daily_withdrawal_limit` in `rs-dpp` is the reference case:
`v0` derives the limit from the current total credits, `v2` reads
`daily_withdrawal_limit_percent` and `max_daily_withdrawal_amount` from
`SystemLimits`. Raising the percentage later is a `SYSTEM_LIMITS_V5`, not a
`v3`.
`SystemLimits`. Raising the percentage later is a table edit (the next
`SYSTEM_LIMITS_V*`, or the unreleased version's own table), not a `v3`.

Limits that only exist from a certain protocol version are `Option`s, `None`
on every table that predates them. `SystemLimits` carries four such fields for
the contract-code capable contract transitions introduced with protocol
version 17: `max_contract_code_state_transition_size` (the wire cap of the
family), `max_contract_code_state_transition_decode_budget` (the bincode budget
it decodes under), `max_contract_code_bundle_bytes` and
`max_contract_code_modules_per_bundle` (the bounds of the bundle itself). A
Comment thread
DCG-Claude marked this conversation as resolved.
reader of the family cap falls back to `max_state_transition_size` where the
option is `None`, so the contract families are bounded like every other one on
older versions.

`ConsensusVersions` holds the Tenderdash consensus parameters Drive owns. Next
to `tenderdash_consensus_version` it carries `block_max_bytes` and
`block_max_gas`, both `None` until protocol version 17; `consensus_params_update`
v2 pushes them to Tenderdash when the new protocol version sets them and the
previous one did not carry the same pair, which is how a block size change is
adopted by every validator at the same height.

## How Subsystem Version Constants Compose

Expand Down Expand Up @@ -588,7 +606,7 @@ version/
storage/, signature/, processing/, ... # per-group tables, each with its own v*.rs
system_limits/
mod.rs # SystemLimits struct
v1.rs .. v4.rs
v1.rs .. v5.rs
system_data_contract_versions/
mod.rs
v1.rs .. v3.rs
Expand Down
4 changes: 4 additions & 0 deletions book/src/versioning/platform-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion {
system_limits: SYSTEM_LIMITS_V1,
consensus: ConsensusVersions {
tenderdash_consensus_version: 0,
block_max_bytes: None,
block_max_gas: None,
},
};
```
Expand Down Expand Up @@ -195,6 +197,8 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion {
system_limits: SYSTEM_LIMITS_V4, // changed: relative daily withdrawal limit + time-range overlap cap
consensus: ConsensusVersions {
tenderdash_consensus_version: 1,
block_max_bytes: None,
block_max_gas: None,
},
};
```
Expand Down
14 changes: 12 additions & 2 deletions packages/dashmate/configs/defaults/getBaseConfigFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,10 @@ export default function getBaseConfigFactory() {
allowlistOnly: false,
flushThrottleTimeout: '100ms',
maxPacketMsgPayloadSize: 10240,
sendRate: 5120000,
recvRate: 5120000,
// Provisional: a 32 MiB contract-code state transition takes about 1.6 s per hop
// at this rate (7 s at the previous 5,120,000); confirmed on the testnet rehearsal.
sendRate: 20480000,
recvRate: 20480000,
maxConnections: 64,
maxOutgoingConnections: 30,
},
Expand All @@ -394,6 +396,9 @@ export default function getBaseConfigFactory() {
port: 26657,
maxOpenConnections: 900,
timeoutBroadcastTx: 0,
// Largest JSON-RPC request body: a 32 MiB contract-code state transition arrives
// base64-encoded (44.7 MB) inside broadcast_tx_sync, plus the JSON envelope.
maxBodyBytes: 50000000,
},
pprof: {
enabled: false,
Expand All @@ -408,6 +413,11 @@ export default function getBaseConfigFactory() {
cacheSize: 15000,
size: 5000,
maxTxsBytes: 1073741824,
// Largest single transaction the mempool accepts. Must be at least the largest
// state transition family cap of the protocol version Drive runs: 32 MiB for the
// contract-code capable contract transitions from protocol version 17; every
// other family stays at 20 KiB and Drive enforces both.
maxTxBytes: 33554432,
timeoutCheckTx: '1s',
txEnqueueTimeout: '10ms',
txSendRateLimit: 10,
Expand Down
37 changes: 37 additions & 0 deletions packages/dashmate/configs/getConfigFileMigrationsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,43 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs)

return configFile;
},
'5.0.0': (configFile) => {
// Protocol version 17 lets the contract create and update transitions carry up to
// 32 MiB of contract code. Tenderdash's per-transaction and RPC body limits are
// node-local and were hard-coded in the template until now; they become options with
// defaults sized for that envelope, and the peer bandwidth caps rise so a maximal
// transaction gossips in seconds rather than tens of seconds. An operator-tuned
// bandwidth cap is left alone.
const previousBaseSendRate = 5120000;
const previousBaseRecvRate = 5120000;

Object.entries(configFile.configs)
.forEach(([, options]) => {
const tenderdash = options.platform?.drive?.tenderdash;
if (!tenderdash) {
return;
}

if (tenderdash.mempool && tenderdash.mempool.maxTxBytes === undefined) {
tenderdash.mempool.maxTxBytes = base.getStored('platform.drive.tenderdash.mempool.maxTxBytes');
}

if (tenderdash.rpc && tenderdash.rpc.maxBodyBytes === undefined) {
tenderdash.rpc.maxBodyBytes = base.getStored('platform.drive.tenderdash.rpc.maxBodyBytes');
}

if (tenderdash.p2p) {
if (tenderdash.p2p.sendRate === previousBaseSendRate) {
tenderdash.p2p.sendRate = base.getStored('platform.drive.tenderdash.p2p.sendRate');
}
if (tenderdash.p2p.recvRate === previousBaseRecvRate) {
tenderdash.p2p.recvRate = base.getStored('platform.drive.tenderdash.p2p.recvRate');
}
}
});

return configFile;
},
'4.1.1': (configFile) => {
// The drive and rs-dapi tags are derived from the package version, and
// the migration that re-pins them no longer fires for a config already
Expand Down
11 changes: 7 additions & 4 deletions packages/dashmate/docs/config/tenderdash.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ These settings control the peer-to-peer network for Tenderdash nodes:
| `platform.drive.tenderdash.p2p.allowlistOnly` | Only allow peers from `persistentPeers` and `seeds` | `false` | `true` |
| `platform.drive.tenderdash.p2p.flushThrottleTimeout` | Throttle timeout for P2P data | `100ms` | `200ms` |
| `platform.drive.tenderdash.p2p.maxPacketMsgPayloadSize` | Maximum P2P message size | `10240` | `20480` |
| `platform.drive.tenderdash.p2p.sendRate` | P2P send rate limit | `5120000` | `10240000` |
| `platform.drive.tenderdash.p2p.recvRate` | P2P receive rate limit | `5120000` | `10240000` |
| `platform.drive.tenderdash.p2p.sendRate` | P2P send rate limit in bytes per second | `20480000` | `10240000` |
| `platform.drive.tenderdash.p2p.recvRate` | P2P receive rate limit in bytes per second | `20480000` | `10240000` |
| `platform.drive.tenderdash.p2p.maxConnections` | Maximum P2P connections | `64` | `128` |
| `platform.drive.tenderdash.p2p.maxOutgoingConnections` | Maximum outgoing P2P connections | `30` | `60` |

Expand Down Expand Up @@ -74,6 +74,7 @@ These settings control the RPC interface for Tenderdash:
| `platform.drive.tenderdash.rpc.host` | Host binding for RPC | `127.0.0.1` | `0.0.0.0` |
| `platform.drive.tenderdash.rpc.maxOpenConnections` | Maximum RPC connections | `900` | `1800` |
| `platform.drive.tenderdash.rpc.timeoutBroadcastTx` | Timeout for broadcasting transactions | `0` | `30s` |
| `platform.drive.tenderdash.rpc.maxBodyBytes` | Maximum RPC request body size in bytes. A state transition arrives base64-encoded inside `broadcast_tx_sync`, so this must hold the largest state transition inflated by a third plus the JSON envelope | `50000000` | `60000000` |

The RPC interface is used for:
- Querying blockchain state
Expand Down Expand Up @@ -103,7 +104,8 @@ The mempool handles pending transactions before they are added to blocks:
|--------|-------------|---------|---------|
| `platform.drive.tenderdash.mempool.size` | Maximum number of transactions in mempool | `5000` | `10000` |
| `platform.drive.tenderdash.mempool.cacheSize` | Size of mempool cache | `10000` | `20000` |
| `platform.drive.tenderdash.mempool.maxTxsBytes` | Maximum transaction size in bytes | `1048576` | `2097152` |
| `platform.drive.tenderdash.mempool.maxTxsBytes` | Maximum total size of all transactions in the mempool in bytes | `1073741824` | `2147483648` |
| `platform.drive.tenderdash.mempool.maxTxBytes` | Maximum size of a single transaction in bytes. Must be at least the largest state transition family cap of the protocol version Drive runs (32 MiB for contract-code capable contract transitions from protocol version 17); Drive enforces the per-family caps itself | `33554432` | `67108864` |
| `platform.drive.tenderdash.mempool.timeoutCheckTx` | Timeout for checking transactions | `1s` | `2s` |
| `platform.drive.tenderdash.mempool.txEnqueueTimeout` | Timeout for enqueueing transactions | `1s` | `2s` |
| `platform.drive.tenderdash.mempool.txSendRateLimit` | Rate limit for sending transactions | `0` | `1000` |
Expand All @@ -118,7 +120,8 @@ Mempool configuration example:
"mempool": {
"size": 5000,
"cacheSize": 10000,
"maxTxsBytes": 1048576,
"maxTxsBytes": 1073741824,
"maxTxBytes": 33554432,
"timeoutCheckTx": "1s",
"txEnqueueTimeout": "1s",
"txSendRateLimit": 0,
Expand Down
12 changes: 10 additions & 2 deletions packages/dashmate/src/config/configJsonSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,10 @@ export default {
type: 'integer',
minimum: 0,
},
maxTxBytes: {
type: 'integer',
minimum: 1,
},
timeoutCheckTx: {
$ref: '#/definitions/duration',
},
Expand Down Expand Up @@ -1228,7 +1232,7 @@ export default {
},
},
additionalProperties: false,
required: ['size', 'maxTxsBytes', 'cacheSize', 'timeoutCheckTx', 'txEnqueueTimeout', 'txSendRateLimit', 'txRecvRateLimit', 'maxConcurrentCheckTx', 'ttlDuration', 'ttlNumBlocks'],
required: ['size', 'maxTxsBytes', 'maxTxBytes', 'cacheSize', 'timeoutCheckTx', 'txEnqueueTimeout', 'txSendRateLimit', 'txRecvRateLimit', 'maxConcurrentCheckTx', 'ttlDuration', 'ttlNumBlocks'],
},
consensus: {
type: 'object',
Expand Down Expand Up @@ -1331,8 +1335,12 @@ export default {
timeoutBroadcastTx: {
$ref: '#/definitions/duration',
},
maxBodyBytes: {
type: 'integer',
minimum: 1,
},
},
required: ['host', 'port', 'maxOpenConnections', 'timeoutBroadcastTx'],
required: ['host', 'port', 'maxOpenConnections', 'timeoutBroadcastTx', 'maxBodyBytes'],
additionalProperties: false,
},
pprof: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ timeout-broadcast-tx-commit = "10s"
timeout-broadcast-tx = "{{=it.platform.drive.tenderdash.rpc.timeoutBroadcastTx}}"

# Maximum size of request body, in bytes
max-body-bytes = 1000000
max-body-bytes = {{= it.platform.drive.tenderdash.rpc.maxBodyBytes }}

# Maximum size of request header, in bytes
max-header-bytes = 1048576
Expand Down Expand Up @@ -386,7 +386,7 @@ tx-recv-rate-punish-peer = false

# Maximum size of a single transaction.
# NOTE: the max size of a tx transmitted over the network is {max-tx-bytes}.
max-tx-bytes = 20480
max-tx-bytes = {{= it.platform.drive.tenderdash.mempool.maxTxBytes }}

# Maximum size of a batch of transactions to send to a peer
# Including space needed by encoding (one varint per transaction).
Expand Down
Loading
Loading