Skip to content

Commit 790ad5b

Browse files
committed
blocktx
1 parent 080d9b2 commit 790ad5b

6 files changed

Lines changed: 220 additions & 11 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# FetchBlock tx deserialisation phases
2+
3+
Populates `Block.body.tx` in the FetchBlock response.
4+
Each phase compiles independently and can be reviewed as a separate commit.
5+
6+
## Background
7+
8+
FetchBlock currently returns `nativeBytes` (raw CBOR), `header` (slot, hash, height), and `timestamp`.
9+
The `body.tx` field is empty.
10+
The proto `Tx` message has 14 fields - implementing them all at once is too large to review.
11+
This plan breaks the work into incremental phases, each adding a subset of fields.
12+
13+
The parsed block is already available via `Consensus.GetBlock` in the same `getBlockComponent` call.
14+
`fromConsensusBlock` + `getBlockTxs` gives `[Tx era]` - no new consensus APIs needed.
15+
16+
## Existing building blocks in Type.hs
17+
18+
- `txOutToUtxoRpcTxOutput` - TxOutput (address, coin, assets, datum, script)
19+
- `scriptDataToUtxoRpcPlutusData` - PlutusData
20+
- `simpleScriptToUtxoRpcNativeScript` - NativeScript
21+
- `referenceScriptToUtxoRpcScript` - Script (reference scripts)
22+
- `utxoRpcBigIntToInteger` - BigInt (inverse)
23+
- `scriptWitnessIndexToRedeemerPurpose` - redeemer purpose mapping
24+
- `mkProtoRedeemer` - Redeemer construction
25+
26+
Missing: TxInput, Withdrawal, Certificate, WitnessSet, AuxData, Collateral, Multiasset (for mint), GovernanceActionProposal, and the top-level `Tx era -> Proto Tx` composition.
27+
28+
## Phase 1: Scaffold - extract txs from block, populate hash + fee
29+
30+
Minimal end-to-end wiring with the simplest fields.
31+
32+
- Change `fetchBlock` to also return the parsed `BlockInMode` (via `fromConsensusBlock` on `GetBlock`)
33+
- In `fetchBlockMethod`, call `getBlockTxs` to get `[Tx era]`
34+
- For each tx, populate:
35+
- `hash` - `getTxId` serialised to raw bytes
36+
- `fee` - from `txFee` on the tx body
37+
- `successful` - `True` (all blocks in ChainDB have validated txs; phase 1 does not distinguish collateral-return scripts)
38+
- Set `Block.body.tx` on the response
39+
- Add an E2E test asserting tx count, hash, and fee for a submitted tx
40+
41+
Build: `cabal build cardano-rpc`
42+
43+
## Phase 2: Inputs, outputs, reference inputs
44+
45+
The core spending data. Reuses existing `txOutToUtxoRpcTxOutput`.
46+
47+
- Add `txInputToProto :: TxIn -> Proto TxInput` (tx_hash + output_index, without as_output or redeemer - those come later)
48+
- Map `txIns` -> `Tx.inputs`
49+
- Map `txOuts` -> `Tx.outputs` (reuse `txOutToUtxoRpcTxOutput`)
50+
- Map `txInsReference` -> `Tx.reference_inputs`
51+
- Extend E2E test to assert input/output fields
52+
53+
Build: `cabal build cardano-rpc`
54+
55+
## Phase 3: Validity, mint, withdrawals
56+
57+
Simple scalar/list fields.
58+
59+
- Map `txValidityLowerBound` + `txValidityUpperBound` -> `Tx.validity` (TxValidity: start, ttl)
60+
- Map `txMintValue` -> `Tx.mint` (repeated Multiasset: policy_id + assets)
61+
- Map `txWithdrawals` -> `Tx.withdrawals` (repeated Withdrawal: reward_account + coin, no redeemer yet)
62+
- Extend E2E test with a minting tx
63+
64+
Build: `cabal build cardano-rpc`
65+
66+
## Phase 4: Collateral
67+
68+
- Map `txInsCollateral` -> `Collateral.collateral`
69+
- Map `txReturnCollateral` -> `Collateral.collateral_return`
70+
- Map `txTotalCollateral` -> `Collateral.total_collateral`
71+
- Set `Tx.collateral`
72+
- `Tx.successful` - refine: check `IsValid` flag on Alonzo+ txs
73+
74+
Build: `cabal build cardano-rpc`
75+
76+
## Phase 5: Witnesses
77+
78+
- Map vkey witnesses -> `WitnessSet.vkeywitness`
79+
- Map scripts (Plutus + native) -> `WitnessSet.script`
80+
- Map plutus datums -> `WitnessSet.plutus_datums` (reuse `scriptDataToUtxoRpcPlutusData`)
81+
- Map redeemers -> `WitnessSet.redeemers` (reuse `mkProtoRedeemer`)
82+
- Map bootstrap witnesses -> `WitnessSet.bootstrapWitnesses`
83+
- Wire redeemers to `TxInput.redeemer` and `Withdrawal.redeemer`
84+
85+
Build: `cabal build cardano-rpc`
86+
87+
## Phase 6: Certificates
88+
89+
The largest proto message (19 oneof variants).
90+
91+
- Map each `TxCert era` variant to the corresponding `Certificate` oneof
92+
- Pre-Conway certs: stake registration/deregistration, delegation, pool registration/retirement, genesis delegation, MIR
93+
- Conway+ certs: reg, unreg, vote deleg, stake vote deleg, committee hot/cold, DRep reg/unreg/update
94+
- Wire certificate redeemers
95+
96+
Build: `cabal build cardano-rpc`
97+
98+
## Phase 7: Auxiliary data + governance proposals
99+
100+
- Map tx metadata -> `AuxData.metadata`
101+
- Map auxiliary scripts -> `AuxData.scripts`
102+
- Map governance action proposals -> `Tx.proposals` (Conway+)
103+
- This completes all 14 Tx fields
104+
105+
Build: `cabal build cardano-rpc`
106+
107+
## Phase dependency graph
108+
109+
```
110+
Phase 1 (scaffold: hash, fee, successful)
111+
|
112+
v
113+
Phase 2 (inputs, outputs, reference inputs)
114+
|
115+
v
116+
Phase 3 (validity, mint, withdrawals)
117+
|
118+
v
119+
Phase 4 (collateral, successful refinement)
120+
|
121+
v
122+
Phase 5 (witnesses + redeemer wiring)
123+
|
124+
v
125+
Phase 6 (certificates)
126+
|
127+
v
128+
Phase 7 (auxiliary data, governance proposals)
129+
```
130+
131+
## Design decisions
132+
133+
- **Incremental field population.**
134+
Each phase adds fields to the same `Tx` proto message.
135+
Unset proto fields default to empty/zero, so partial responses are valid.
136+
137+
- **No new consensus APIs.**
138+
`fromConsensusBlock` + `getBlockTxs` already exist in cardano-api.
139+
The work is entirely in the cardano-rpc mapping layer.
140+
141+
- **Reuse existing conversions.**
142+
`txOutToUtxoRpcTxOutput`, `scriptDataToUtxoRpcPlutusData`, `mkProtoRedeemer` etc. are already battle-tested in the Query and Submit handlers.
143+
144+
- **Witnesses and redeemers split from inputs/withdrawals.**
145+
Phase 2-3 populate inputs and withdrawals without redeemers.
146+
Phase 5 adds the witness set and wires redeemers back to their inputs/withdrawals.
147+
This avoids a large cross-cutting change.
148+
149+
- **Certificates are a separate phase.**
150+
The Certificate oneof has 19 variants spanning pre-Conway and Conway eras.
151+
Isolating it keeps reviews focused.
152+
153+
- **Dijkstra safety.**
154+
`conwayEraOnwardsConstraints` crashes at runtime for Dijkstra.
155+
Phases 6-7 (certificates, governance proposals) must use
156+
`caseShelleyToBabbageOrConwayOrDijkstra` and pattern match on concrete
157+
`ConwayEraOnwards` constructors in the right arm.
158+
The cardano-api lenses `proposalProceduresTxBodyL` and `votingProceduresTxBodyL`
159+
use `conwayEraOnwardsConstraints` internally - avoid them, go through the ledger
160+
tx body directly with explicit era constraints.
161+
162+
- **Work through the ledger tx directly.**
163+
`ShelleyTxBody` is deprecated.
164+
Access the ledger `Tx` via the `ShelleyTx` constructor and use ledger lenses
165+
(`bodyTxL`, `witsTxL`, `auxDataTxL`, `isValidTxL`) rather than cardano-api wrappers.

cardano-rpc/src/Cardano/Rpc/Proto/Api/UtxoRpc/Sync.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ module Cardano.Rpc.Proto.Api.UtxoRpc.Sync
55
( module Proto.Utxorpc.V1beta.Sync.Sync
66
, module Proto.Utxorpc.V1beta.Sync.Sync_Fields
77
, module Proto.Utxorpc.V1beta.Cardano.Cardano
8-
, header
8+
, module Proto.Utxorpc.V1beta.Cardano.Cardano_Fields
99
)
1010
where
1111

1212
import Network.GRPC.Common
1313
import Network.GRPC.Common.Protobuf
1414

1515
import Proto.Utxorpc.V1beta.Cardano.Cardano
16-
import Proto.Utxorpc.V1beta.Cardano.Cardano_Fields (header)
16+
import Proto.Utxorpc.V1beta.Cardano.Cardano_Fields hiding
17+
( hash
18+
, height
19+
, slot
20+
, timestamp
21+
)
1722
import Proto.Utxorpc.V1beta.Sync.Sync
1823
import Proto.Utxorpc.V1beta.Sync.Sync_Fields
1924

cardano-rpc/src/Cardano/Rpc/Server/Internal/UtxoRpc/Predicate.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
module Cardano.Rpc.Server.Internal.UtxoRpc.Predicate
66
( matchesUtxoPredicate
7+
, exactAddressPredicate
78
, extractAddressesFromPredicate
89
, matchesAddressPattern
910
, matchesAssetPattern
@@ -24,8 +25,10 @@ import Cardano.Rpc.Proto.Api.UtxoRpc.Query qualified as UtxoRpc
2425
import RIO hiding (toList)
2526

2627
import Data.ByteString qualified as BS
28+
import Data.ProtoLens (defMessage)
2729
import Data.Set qualified as Set
2830
import GHC.IsList
31+
import Network.GRPC.Spec (Proto (..))
2932

3033
-- | Check if a UTxO entry matches a 'UtxoPredicate'.
3134
-- All present fields are combined with AND logic.
@@ -89,6 +92,20 @@ matchesAddressPattern pat addr =
8992
_ -> BS.null $ pat ^. UtxoRpc.delegationPart
9093
_ -> BS.null $ pat ^. UtxoRpc.delegationPart
9194

95+
-- | A 'UtxoPredicate' matching UTxOs at the exact address.
96+
exactAddressPredicate
97+
:: IsCardanoEra era
98+
=> AddressInEra era
99+
-> Proto UtxoRpc.UtxoPredicate
100+
exactAddressPredicate address =
101+
Proto $
102+
defMessage
103+
& UtxoRpc.match
104+
.~ ( defMessage
105+
& UtxoRpc.cardano
106+
.~ (defMessage & UtxoRpc.address .~ (defMessage & UtxoRpc.exactAddress .~ serialiseToRawBytes address))
107+
)
108+
92109
-- | Serialise a 'PaymentCredential' to raw bytes (the key or script hash).
93110
serialisePaymentCredential :: PaymentCredential -> ByteString
94111
serialisePaymentCredential (PaymentCredentialByKey h) = serialiseToRawBytes h

cardano-rpc/src/Cardano/Rpc/Server/Internal/UtxoRpc/Sync.hs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import Cardano.Rpc.Proto.Api.UtxoRpc.Sync qualified as U5c
1515
import Cardano.Rpc.Server.Internal.Error
1616
import Cardano.Rpc.Server.Internal.Monad
1717
import Cardano.Rpc.Server.Internal.Tracing ()
18+
import Cardano.Rpc.Server.Internal.UtxoRpc.Type (txToUtxoRpcTx)
1819
import Cardano.Rpc.Server.NodeKernelAccess
1920

2021
import RIO
2122

2223
import Data.ByteString qualified as BS
2324
import Data.ProtoLens (defMessage)
2425
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
25-
import Network.GRPC.Spec (GrpcError (GrpcInternal, GrpcInvalidArgument, GrpcNotFound), Proto)
26+
import Network.GRPC.Spec (GrpcError (GrpcInternal, GrpcInvalidArgument, GrpcNotFound), Proto (..))
2627

2728
-- | Handle the @FetchBlock@ SyncService RPC method.
2829
-- Fetches a block from ChainDB by slot and header hash.
@@ -51,13 +52,15 @@ fetchBlockMethod request = do
5152
headerHash <-
5253
deserialiseFromRawBytes (proxyToAsType (Proxy @(Hash BlockHeader))) hashBytes
5354
& either (const throwInvalidHash) pure
54-
(rawBytes, BlockNo height) <-
55+
(rawBytes, BlockInMode era block) <-
5556
fetchBlock nodeKernelAccess slot headerHash >>= maybe throwNotFound pure
5657
eraHistory <- readEraHistory
5758
timestampMs <-
5859
slotToUTCTime systemStart eraHistory slot
5960
& either (const throwPastHorizon) (pure . round . (* 1000) . utcTimeToPOSIXSeconds)
60-
let blockHeader =
61+
let BlockHeader _ _ (BlockNo height) = getBlockHeader block
62+
txs = forEraInEon era [] $ \sbe -> Proto . txToUtxoRpcTx sbe <$> getBlockTxs block
63+
blockHeader =
6164
defMessage
6265
& U5c.slot .~ unSlotNo slot
6366
& U5c.hash .~ hashBytes
@@ -68,7 +71,6 @@ fetchBlockMethod request = do
6871
.~ ( defMessage
6972
& U5c.nativeBytes .~ rawBytes
7073
& U5c.cardano . U5c.header .~ blockHeader
74+
& U5c.cardano . U5c.body . U5c.tx .~ txs
7175
& U5c.cardano . U5c.timestamp .~ timestampMs
7276
)
73-
74-
-- TODO: cardano.body.tx - needs full block deserialisation + UTxO RPC tx mapping

cardano-rpc/src/Cardano/Rpc/Server/Internal/UtxoRpc/Type.hs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ module Cardano.Rpc.Server.Internal.UtxoRpc.Type
1515
, txInTxOutToAnyUtxoData
1616
, anyUtxoDataUtxoRpcToUtxo
1717
, txOutToUtxoRpcTxOutput
18+
, txToUtxoRpcTx
19+
, txoRefUtxoRpcToTxIn
1820
, utxoRpcTxOutputToTxOut
1921
, protocolParamsToUtxoRpcPParams
2022
, simpleScriptToUtxoRpcNativeScript
@@ -661,6 +663,23 @@ utxoRpcTxOutputToTxOut txOutput = do
661663
datum
662664
referenceScript
663665

666+
-- | Convert a transaction from a fetched block to the UTxO RPC 'UtxoRpc.Tx' message.
667+
-- Populates hash, fee and successful.
668+
-- TODO: inputs, outputs, certificates, withdrawals, mint, reference inputs,
669+
-- witnesses, collateral, validity, auxiliary data and proposals are not mapped yet.
670+
txToUtxoRpcTx
671+
:: ShelleyBasedEra era
672+
-> Tx era
673+
-> UtxoRpc.Tx
674+
txToUtxoRpcTx sbe (ShelleyTx _ ledgerTx) =
675+
shelleyBasedEraConstraints sbe $
676+
defMessage
677+
& U5c.hash .~ serialiseToRawBytes (fromShelleyTxId (L.txIdTx ledgerTx))
678+
& U5c.fee .~ getProto (inject (ledgerTx ^. L.bodyTxL . L.feeTxBodyL))
679+
-- all transactions in a block stored in ChainDB have been validated;
680+
-- TODO: reflect the IsValid flag for collateral-consuming transactions
681+
& U5c.successful .~ True
682+
664683
utxoRpcBigIntToInteger
665684
:: forall m
666685
. HasCallStack

cardano-rpc/src/Cardano/Rpc/Server/NodeKernelAccess.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ grabNodeKernelAccess =
7878
Just nodeKernelAccess ->
7979
pure nodeKernelAccess
8080

81-
-- | Fetch a raw block and its block number from ChainDB by slot and header hash.
81+
-- | Fetch a raw block and its parsed era-contextualised form from ChainDB
82+
-- by slot and header hash.
8283
fetchBlock
8384
:: MonadIO m
8485
=> NodeKernelAccess
@@ -87,9 +88,9 @@ fetchBlock
8788
-- ^ Block slot number
8889
-> Hash BlockHeader
8990
-- ^ Block header hash
90-
-> m (Maybe (ByteString, BlockNo))
91-
-- ^ Raw CBOR bytes and block number, or 'Nothing' if not found
91+
-> m (Maybe (ByteString, BlockInMode))
92+
-- ^ Raw CBOR bytes and the block in era context, or 'Nothing' if not found
9293
fetchBlock NodeKernelAccess{chainDb} slot (HeaderHash shortHash) = do
9394
let point = Consensus.RealPoint slot (Consensus.OneEraHash shortHash)
94-
component = (,) <$> fmap BSL.toStrict Consensus.GetRawBlock <*> fmap Consensus.blockNo Consensus.GetBlock
95+
component = (,) <$> fmap BSL.toStrict Consensus.GetRawBlock <*> fmap fromConsensusBlock Consensus.GetBlock
9596
liftIO $ Consensus.getBlockComponent chainDb component point

0 commit comments

Comments
 (0)