Bug description
Under the P-COOL flow, an immutable object used as a transaction input acquires an owned-object lock that is never released, so only the first transaction in an epoch can read it. Every later transaction referencing the same immutable object is rejected with ObjectLockConflict.
post_consensus_validation::extract_owned_input_objects decides what counts as an owned input from the raw transaction alone — every InputObjectKind::ImmOrOwnedMoveObject qualifies:
.filter_map(|input| match input {
InputObjectKind::ImmOrOwnedMoveObject(obj_ref) => Some(obj_ref),
InputObjectKind::SharedMoveObject { .. } => None,
InputObjectKind::MovePackage(_) => None,
})
It never loads the object, so unlike the pre-consensus ObjectReadResult::get_owned_objref it cannot apply the if object.is_immutable() { None } filter that the same comment ("Return the object ref iff the object is an owned object (i.e. not shared, not immutable)") promises.
The lock then goes into the per-epoch owned_object_locked_transactions table keyed by the full ObjectReference, and entries are only ever inserted. A lock normally lapses because the object takes a new version and the old reference stops being used. An immutable object's reference never changes, so the lock stands until the epoch ends.
This is reachable only with enable_pcool_flow, which protocol version 32 enables for every chain that is not mainnet or testnet, so today it affects local and dev networks. It would affect testnet and mainnet if the flag is ever enabled there.
Rust version
- Rust version:
rustc 1.98.0-nightly (3daae5e42 2026-06-14) (client side only — the node is the published release binary)
Version
- Version number, commit, or branch:
v1.29.0-rc (63af87fdf21d88d6b4e5e150ec263b70732dc5ec), via the iotaledger/iota-tools:v1.29.0-rc image
Hardware specification
- Operating system: NixOS 26.11, Linux 7.1.1 x86_64
- RAM: 60 GiB
- Cores: 16
- Device: x86_64 desktop
Steps To reproduce the bug
No Move package or custom object is needed — the IOTA coin's own CoinMetadata is immutable and present at genesis.
-
Start a localnet:
docker run --rm -d -p 9000:9000 -p 9123:9123 --name iota \
iotaledger/iota-tools:v1.29.0-rc \
iota-localnet start --with-faucet --force-regenesis
-
Point the CLI at it, fund an address from the faucet, and find the metadata object:
curl -s -X POST http://127.0.0.1:9000 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"iotax_getCoinMetadata","params":["0x2::iota::IOTA"]}'
-
Read it twice in a row, within one epoch:
iota client ptb --move-call 0x2::coin::get_decimals "<0x2::iota::IOTA>" @<METADATA_ID> --gas-budget 20000000
Expected behaviour
Both calls succeed. An immutable object is read-only and cannot be a source of contention, so any number of transactions should be able to reference it concurrently — as they do with enable_pcool_flow off.
Actual behaviour
The first call succeeds. The second and every subsequent one is rejected against the first call's digest, until the epoch boundary clears the per-epoch lock table.
Confirmed as a P-COOL regression by running two v1.29.0-rc localnets side by side from the same image. With the flag overridden off (IOTA_PROTOCOL_CONFIG_OVERRIDE_ENABLE=1, IOTA_PROTOCOL_CONFIG_FEATURE_FLAGS_OVERRIDE_ENABLE_PCOOL_FLOW=false, both still reporting protocolVersion 32), three consecutive reads all succeed. With the stock configuration, the second read fails.
We first hit this in a test that creates a collateral vault: one transaction proposes it and a second realizes it, and both take the same CoinMetadata<T> as &CoinMetadata. The second was rejected roughly three seconds after the first succeeded.
Errors
Invalid transaction: unknown error: Transaction was rejected as invalid by more than 1/3 of
validator stake during submission (non-retriable): Object ObjectReference {
object_id: ObjectId("0x9d0764849f59eef9ad7acc1e5cf6bd2172e16ace38fb80dde66f7365333b6548"),
version: Version(86),
digest: ObjectDigest("4MbCcugXzHfTbwNpZmEipwuHU9PC1ZGj4qJMBztEumji")
} already locked by a different transaction: DyZbDxjjRFn7MRwYbF5T1YAwrCc8j6VZNW3K5YPrSaGo
The named object is immutable:
objectId 0x9d0764849f59eef9ad7acc1e5cf6bd2172e16ace38fb80dde66f7365333b6548
version 86
objType 0x2::coin::CoinMetadata<...::stable::STABLE>
owner Immutable
Bug description
Under the P-COOL flow, an immutable object used as a transaction input acquires an owned-object lock that is never released, so only the first transaction in an epoch can read it. Every later transaction referencing the same immutable object is rejected with
ObjectLockConflict.post_consensus_validation::extract_owned_input_objectsdecides what counts as an owned input from the raw transaction alone — everyInputObjectKind::ImmOrOwnedMoveObjectqualifies:It never loads the object, so unlike the pre-consensus
ObjectReadResult::get_owned_objrefit cannot apply theif object.is_immutable() { None }filter that the same comment ("Return the object ref iff the object is an owned object (i.e. not shared, not immutable)") promises.The lock then goes into the per-epoch
owned_object_locked_transactionstable keyed by the fullObjectReference, and entries are only ever inserted. A lock normally lapses because the object takes a new version and the old reference stops being used. An immutable object's reference never changes, so the lock stands until the epoch ends.This is reachable only with
enable_pcool_flow, which protocol version 32 enables for every chain that is not mainnet or testnet, so today it affects local and dev networks. It would affect testnet and mainnet if the flag is ever enabled there.Rust version
rustc 1.98.0-nightly (3daae5e42 2026-06-14)(client side only — the node is the published release binary)Version
v1.29.0-rc(63af87fdf21d88d6b4e5e150ec263b70732dc5ec), via theiotaledger/iota-tools:v1.29.0-rcimageHardware specification
Steps To reproduce the bug
No Move package or custom object is needed — the IOTA coin's own
CoinMetadatais immutable and present at genesis.Start a localnet:
Point the CLI at it, fund an address from the faucet, and find the metadata object:
Read it twice in a row, within one epoch:
Expected behaviour
Both calls succeed. An immutable object is read-only and cannot be a source of contention, so any number of transactions should be able to reference it concurrently — as they do with
enable_pcool_flowoff.Actual behaviour
The first call succeeds. The second and every subsequent one is rejected against the first call's digest, until the epoch boundary clears the per-epoch lock table.
Confirmed as a P-COOL regression by running two
v1.29.0-rclocalnets side by side from the same image. With the flag overridden off (IOTA_PROTOCOL_CONFIG_OVERRIDE_ENABLE=1,IOTA_PROTOCOL_CONFIG_FEATURE_FLAGS_OVERRIDE_ENABLE_PCOOL_FLOW=false, both still reportingprotocolVersion 32), three consecutive reads all succeed. With the stock configuration, the second read fails.We first hit this in a test that creates a collateral vault: one transaction proposes it and a second realizes it, and both take the same
CoinMetadata<T>as&CoinMetadata. The second was rejected roughly three seconds after the first succeeded.Errors
The named object is immutable: