Skip to content

Commit f6f66ee

Browse files
committed
add another scenario for offchain vote data
1 parent c200d1d commit f6f66ee

File tree

7 files changed

+112
-20
lines changed

7 files changed

+112
-20
lines changed

cardano-db-sync/app/http-get-json-metadata.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,12 @@ runGetVote :: Text.Text -> Maybe VoteMetaHash -> DB.AnchorType -> IO ()
143143
runGetVote file mExpectedHash vtype = do
144144
respBs <- BS.readFile (Text.unpack file)
145145
let respLBs = fromStrict respBs
146-
(mocvd, val, hsh, mWarning) <- runOrThrowIO $ runExceptT $ parseAndValidateVoteData respBs respLBs mExpectedHash vtype Nothing
146+
(mocvd, val, hsh, mWarning, isValidJson) <- runOrThrowIO $ runExceptT $ parseAndValidateVoteData respBs respLBs mExpectedHash vtype Nothing
147147
print mocvd
148148
print val
149149
print $ bsBase16Encode hsh
150150
print mWarning
151+
putStrLn $ "Is valid JSON: " ++ show isValidJson
151152

152153
-- ------------------------------------------------------------------------------------------------
153154

cardano-db-sync/src/Cardano/DbSync/OffChain.hs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,9 @@ fetchOffChainVoteData gateways time oVoteWorkQ =
349349
convert eres =
350350
case eres of
351351
Right sVoteData ->
352-
case sovaOffChainVoteData sVoteData of
353-
Just offChainData ->
352+
case (sovaIsValidJson sVoteData, sovaOffChainVoteData sVoteData) of
353+
-- Scenario 1: Valid JSON + Valid CIP schema
354+
(True, Just offChainData) ->
354355
let
355356
minimalBody = Vote.getMinimalBody offChainData
356357
vdt =
@@ -371,7 +372,8 @@ fetchOffChainVoteData gateways time oVoteWorkQ =
371372
externalUpdatesF ocvdId = map (mkexternalUpdates ocvdId) $ mListToList $ Vote.externalUpdates minimalBody
372373
in
373374
OffChainVoteResultMetadata vdt (OffChainVoteAccessors gaF drepF authorsF referencesF externalUpdatesF)
374-
Nothing ->
375+
-- Scenario 2: Valid JSON but invalid CIP schema
376+
(True, Nothing) ->
375377
let
376378
vdt =
377379
DB.OffChainVoteData
@@ -391,6 +393,27 @@ fetchOffChainVoteData gateways time oVoteWorkQ =
391393
externalUpdatesF _ = []
392394
in
393395
OffChainVoteResultMetadata vdt (OffChainVoteAccessors gaF drepF authorsF referencesF externalUpdatesF)
396+
-- Scenario 3: Invalid JSON (hash matches but content is not parseable as JSON)
397+
(False, _) ->
398+
let
399+
vdt =
400+
DB.OffChainVoteData
401+
{ DB.offChainVoteDataLanguage = ""
402+
, DB.offChainVoteDataComment = Nothing
403+
, DB.offChainVoteDataBytes = sovaBytes sVoteData
404+
, DB.offChainVoteDataHash = sovaHash sVoteData
405+
, DB.offChainVoteDataJson = sovaJson sVoteData -- This will be the error message JSON
406+
, DB.offChainVoteDataVotingAnchorId = oVoteWqReferenceId oVoteWorkQ
407+
, DB.offChainVoteDataWarning = sovaWarning sVoteData
408+
, DB.offChainVoteDataIsValid = Nothing -- NULL for unparseable JSON
409+
}
410+
gaF _ = Nothing
411+
drepF _ = Nothing
412+
authorsF _ = []
413+
referencesF _ = []
414+
externalUpdatesF _ = []
415+
in
416+
OffChainVoteResultMetadata vdt (OffChainVoteAccessors gaF drepF authorsF referencesF externalUpdatesF)
394417
Left err ->
395418
OffChainVoteResultError $
396419
DB.OffChainVoteFetchError

cardano-db-sync/src/Cardano/DbSync/OffChain/Http.hs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ httpGetOffChainVoteDataSingle vurl metaHash anchorType = do
108108
let req = httpGetBytes manager request 3000000 3000000 url
109109
httpRes <- handleExceptT (convertHttpException url) req
110110
(respBS, respLBS, mContentType) <- hoistEither httpRes
111-
(mocvd, decodedValue, metadataHash, mWarning) <- parseAndValidateVoteData respBS respLBS metaHash anchorType (Just $ OffChainVoteUrl vurl)
111+
(mocvd, decodedValue, metadataHash, mWarning, isValidJson) <- parseAndValidateVoteData respBS respLBS metaHash anchorType (Just $ OffChainVoteUrl vurl)
112112
pure $
113113
SimplifiedOffChainVoteData
114114
{ sovaHash = metadataHash
@@ -117,11 +117,12 @@ httpGetOffChainVoteDataSingle vurl metaHash anchorType = do
117117
, sovaContentType = mContentType
118118
, sovaOffChainVoteData = mocvd
119119
, sovaWarning = mWarning
120+
, sovaIsValidJson = isValidJson
120121
}
121122
where
122123
url = OffChainVoteUrl vurl
123124

124-
parseAndValidateVoteData :: ByteString -> LBS.ByteString -> Maybe VoteMetaHash -> DB.AnchorType -> Maybe OffChainUrlType -> ExceptT OffChainFetchError IO (Maybe Vote.OffChainVoteData, Aeson.Value, ByteString, Maybe Text)
125+
parseAndValidateVoteData :: ByteString -> LBS.ByteString -> Maybe VoteMetaHash -> DB.AnchorType -> Maybe OffChainUrlType -> ExceptT OffChainFetchError IO (Maybe Vote.OffChainVoteData, Aeson.Value, ByteString, Maybe Text, Bool)
125126
parseAndValidateVoteData bs lbs metaHash anchorType murl = do
126127
let metadataHash = Crypto.digest (Proxy :: Proxy Crypto.Blake2b_256) bs
127128
-- First check if hash matches - this is critical and must fail if mismatch
@@ -130,17 +131,23 @@ parseAndValidateVoteData bs lbs metaHash anchorType murl = do
130131
| metadataHash /= expectedMetaHashBs ->
131132
left $ OCFErrHashMismatch murl (renderByteArray expectedMetaHashBs) (renderByteArray metadataHash)
132133
_ -> pure (metadataHash, Nothing)
133-
-- Hash matches, now decode as generic JSON (this should still fail if not valid JSON)
134-
decodedValue <-
134+
-- Hash matches, now try to decode as generic JSON
135+
-- If this fails, we still want to store the data with is_valid = NULL and an error message
136+
(decodedValue, isValidJson) <-
135137
case Aeson.eitherDecode' @Aeson.Value lbs of
136-
Left err -> left $ OCFErrJsonDecodeFail murl (Text.pack err)
137-
Right res -> pure res
138-
-- Try to decode into strongly-typed vote data structure
138+
Left err ->
139+
-- Not valid JSON - create an error message object
140+
pure (Aeson.object [("error", Aeson.String "Content is not valid JSON. See bytes column for raw data."), ("parse_error", Aeson.String $ Text.pack err)], False)
141+
Right res -> pure (res, True)
142+
-- Try to decode into strongly-typed vote data structure (only if JSON was valid)
139143
-- If this fails (e.g., doNotList is string instead of bool), we still store with is_valid = false
140-
let ocvd = case Vote.eitherDecodeOffChainVoteData lbs anchorType of
141-
Left _err -> Nothing -- Don't fail, just return Nothing (will set is_valid = false)
142-
Right res -> Just res
143-
pure (ocvd, decodedValue, hsh, mWarning)
144+
let ocvd =
145+
if isValidJson
146+
then case Vote.eitherDecodeOffChainVoteData lbs anchorType of
147+
Left _err -> Nothing -- Don't fail, just return Nothing (will set is_valid = false)
148+
Right res -> Just res
149+
else Nothing -- Not valid JSON, so can't parse as CIP
150+
pure (ocvd, decodedValue, hsh, mWarning, isValidJson)
144151

145152
httpGetBytes ::
146153
Http.Manager ->

cardano-db-sync/src/Cardano/DbSync/Types.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ data SimplifiedOffChainVoteData = SimplifiedOffChainVoteData
195195
, sovaContentType :: !(Maybe ByteString)
196196
, sovaOffChainVoteData :: !(Maybe Vote.OffChainVoteData)
197197
, sovaWarning :: !(Maybe Text)
198+
, sovaIsValidJson :: !Bool
198199
}
199200

200201
data Retry = Retry

cardano-db-sync/test/Cardano/DbSync/OffChain/VoteTest.hs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import Cardano.DbSync.Error (runOrThrowIO)
88
import Cardano.DbSync.OffChain.Http (parseAndValidateVoteData)
99
import Cardano.Prelude hiding ((%))
1010
import qualified Data.Aeson as Aeson
11+
import qualified Data.Aeson.Key as AesonKey
12+
import qualified Data.Aeson.KeyMap as KeyMap
1113
import qualified Data.ByteString as BS
1214
import qualified Data.ByteString.Lazy as LBS
15+
import qualified Data.Text as Text
1316
import Hedgehog
1417

1518
tests :: IO Bool
@@ -19,10 +22,12 @@ tests =
1922
"Cardano.DbSync.OffChain.Vote"
2023
[ ("parseAndValidateVoteData handles invalid CIP format", prop_parseInvalidCIPFormat)
2124
, ("parseAndValidateVoteData handles valid JSON but invalid structure", prop_parseValidJsonInvalidStructure)
25+
, ("parseAndValidateVoteData handles unparseable JSON", prop_parseUnparseableJson)
2226
]
2327

2428
-- | Test that we can parse JSON with incorrect field types (e.g., doNotList as string instead of bool)
2529
-- This is based on the issue https://github.com/IntersectMBO/cardano-db-sync/issues/1995
30+
-- Scenario: Valid JSON but invalid CIP schema -> is_valid = false
2631
prop_parseInvalidCIPFormat :: Property
2732
prop_parseInvalidCIPFormat = withTests 1 $ property $ do
2833
-- Read the test file with invalid doNotList field (string instead of bool)
@@ -32,10 +37,11 @@ prop_parseInvalidCIPFormat = withTests 1 $ property $ do
3237
-- Run the parser
3338
result <- liftIO $ runOrThrowIO $ runExceptT $ parseAndValidateVoteData fileContent lbsContent Nothing DB.DrepAnchor Nothing
3439

35-
let (mocvd, val, _hash, _warning) = result
40+
let (mocvd, val, _hash, _warning, isValidJson) = result
3641

3742
-- Should succeed in parsing generic JSON
3843
annotate "Successfully parsed as generic JSON"
44+
assert isValidJson
3945

4046
-- Should fail to parse into strongly-typed OffChainVoteData
4147
assert $ isNothing mocvd
@@ -50,6 +56,7 @@ prop_parseInvalidCIPFormat = withTests 1 $ property $ do
5056
failure
5157

5258
-- | Test with completely valid JSON but not matching the CIP schema
59+
-- Scenario: Valid JSON but invalid CIP schema -> is_valid = false
5360
prop_parseValidJsonInvalidStructure :: Property
5461
prop_parseValidJsonInvalidStructure = property $ do
5562
-- Create a valid JSON that doesn't match CIP schema at all
@@ -60,8 +67,46 @@ prop_parseValidJsonInvalidStructure = property $ do
6067
-- This should succeed because it's valid JSON, just not matching the schema
6168
result <- liftIO $ runOrThrowIO $ runExceptT $ parseAndValidateVoteData bs lbs Nothing DB.DrepAnchor Nothing
6269

63-
let (mocvd, _val, _hash, _warning) = result
70+
let (mocvd, _val, _hash, _warning, isValidJson) = result
6471

6572
annotate "Successfully parsed generic JSON"
73+
assert isValidJson
6674
-- Should not parse into OffChainVoteData
6775
assert $ isNothing mocvd
76+
77+
-- | Test with completely unparseable content (not valid JSON at all)
78+
-- Scenario: Invalid JSON but hash matches -> is_valid = NULL
79+
prop_parseUnparseableJson :: Property
80+
prop_parseUnparseableJson = property $ do
81+
-- Create content that is not valid JSON
82+
let notJson = "This is just plain text, not JSON at all!"
83+
bs = encodeUtf8 notJson
84+
lbs = LBS.fromStrict bs
85+
86+
-- This should not fail, but instead return an error message in the JSON field
87+
result <- liftIO $ runOrThrowIO $ runExceptT $ parseAndValidateVoteData bs lbs Nothing DB.DrepAnchor Nothing
88+
89+
let (mocvd, val, _hash, _warning, isValidJson) = result
90+
91+
annotate "Content is not valid JSON"
92+
-- Should flag as invalid JSON
93+
assert $ not isValidJson
94+
95+
-- Should not parse into OffChainVoteData
96+
assert $ isNothing mocvd
97+
98+
-- Should have an error message in the JSON value
99+
case val of
100+
Aeson.Object obj -> do
101+
annotate "Has error message object"
102+
-- Check that error field exists
103+
case KeyMap.lookup (AesonKey.fromString "error") obj of
104+
Just (Aeson.String msg) -> do
105+
annotate $ "Error message: " <> show msg
106+
assert $ Text.isInfixOf "not valid JSON" msg
107+
_ -> do
108+
annotate "Expected error field with string value"
109+
failure
110+
_ -> do
111+
annotate "Expected JSON object with error message"
112+
failure

cardano-db/src/Cardano/Db/Schema/Core/OffChain.hs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,15 @@ offChainPoolFetchErrorEncoder =
9090

9191
-- |
9292
-- Table Name: off_chain_vote_data
93-
-- Description:
93+
-- Description: Stores off-chain voting anchor data with validation status
94+
--
95+
-- The is_valid column indicates the parsing status:
96+
-- • TRUE: Content is valid JSON AND conforms to CIP schema
97+
-- All related tables are populated
98+
-- • FALSE: Content is valid JSON BUT does not conform to CIP schema
99+
-- The json column contains the actual JSON, but related tables are empty
100+
-- • NULL: Content is not valid JSON at all
101+
-- The json column contains an error message, bytes column has raw data
94102
data OffChainVoteData = OffChainVoteData
95103
{ offChainVoteDataVotingAnchorId :: !Id.VotingAnchorId -- noreference
96104
, offChainVoteDataHash :: !ByteString

doc/schema.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,14 @@ A table containing pool offchain data fetch errors.
10141014

10151015
### `off_chain_vote_data`
10161016

1017-
The table with the offchain metadata related to Vote Anchors. It accepts metadata in a more lenient way than what's decribed in CIP-100. New in 13.2-Conway.
1017+
Stores off-chain voting anchor data with validation status. The table accepts metadata in a more lenient way than what's described in CIP-100. Only data with hash matches are stored here; hash mismatches are stored in off_chain_vote_fetch_error for retry.
1018+
1019+
The is_valid column indicates the parsing status:
1020+
• TRUE: Content is valid JSON AND conforms to CIP-100 schema. All related fields (language, comment) and related tables (off_chain_vote_gov_action_data, off_chain_vote_drep_data, off_chain_vote_author, off_chain_vote_reference, off_chain_vote_external_update) are populated.
1021+
• FALSE: Hash matches and content is valid JSON BUT does not conform to CIP-100 schema. The json column contains the actual JSON, but language/comment fields and related tables remain empty.
1022+
• NULL: Hash matches but content is not valid JSON at all. The json column contains an error message, bytes column has raw data. Language/comment fields and related tables remain empty.
1023+
1024+
New in 13.2-Conway.
10181025

10191026
* Primary Id: `id`
10201027

@@ -1028,7 +1035,7 @@ The table with the offchain metadata related to Vote Anchors. It accepts metadat
10281035
| `json` | jsonb | The payload as JSON. |
10291036
| `bytes` | bytea | The raw bytes of the payload. |
10301037
| `warning` | string | A warning that occured while validating the metadata. |
1031-
| `is_valid` | boolean | False if the data is found invalid. db-sync leaves this field null since it normally populates off_chain_vote_fetch_error for invalid data. It can be used manually to mark some metadata invalid by clients. |
1038+
| `is_valid` | boolean | Indicates validation status: TRUE for valid JSON conforming to CIP-100 schema, FALSE for valid JSON not conforming to CIP-100 schema, NULL for content that is not valid JSON at all. |
10321039

10331040
### `off_chain_vote_gov_action_data`
10341041

0 commit comments

Comments
 (0)