@@ -236,8 +236,23 @@ impl Deserializable for BlockHeader {
236236 None
237237 } ;
238238
239+ // Kerrigan is a multi-algo chain: the mined algorithm is encoded at bits 9-11 of
240+ // the version field. Different algos produce different header tails, so we cache
241+ // the id here and use it to dispatch the per-algo sections below.
242+ // 0 = X11 (standard 80-byte Bitcoin header, no tail)
243+ // 1 = KawPoW (tail: n_height u32 + n_nonce u64 + mix_hash H256, like RVN)
244+ // 2 = Equihash(200,9), 3 = Equihash(192,7) — Zcash-style Sapling layout
245+ let krgn_algo: Option < u32 > = if reader. chain_variant ( ) . is_krgn ( ) {
246+ Some ( ( version >> 9 ) & 0x7u32 )
247+ } else {
248+ None
249+ } ;
250+ let is_krgn_equihash = matches ! ( krgn_algo, Some ( 2 ) | Some ( 3 ) ) ;
251+ let is_krgn_kawpow = matches ! ( krgn_algo, Some ( 1 ) ) ;
252+
239253 let is_zcash_style = ( version == 4 && !reader. chain_variant ( ) . is_btc ( ) && !reader. chain_variant ( ) . is_ppc ( ) )
240- || reader. chain_variant ( ) . is_kmd_assetchain ( ) ;
254+ || reader. chain_variant ( ) . is_kmd_assetchain ( )
255+ || is_krgn_equihash;
241256 let mut hash_final_sapling_root = if is_zcash_style { Some ( reader. read ( ) ?) } else { None } ;
242257 let time = reader. read ( ) ?;
243258 let bits = if is_zcash_style {
@@ -311,11 +326,16 @@ impl Deserializable for BlockHeader {
311326 } ;
312327
313328 // https://github.com/RavenProject/Ravencoin/blob/61c790447a5afe150d9892705ac421d595a2df60/src/primitives/block.h#L67
314- let ( n_height, n_nonce_u64, mix_hash) = if version == KAWPOW_VERSION && reader. chain_variant ( ) . is_rvn ( ) {
315- ( Some ( reader. read ( ) ?) , Some ( reader. read ( ) ?) , Some ( reader. read ( ) ?) )
316- } else {
317- ( None , None , None )
318- } ;
329+ // Also used by Kerrigan KawPoW blocks: identical on-wire layout as RVN's KawPoW
330+ // tail (n_height u32, n_nonce u64, mix_hash H256), just guarded by different
331+ // version bits. Kerrigan keeps the 80-byte prefix nonce (zeroed) on the wire, so
332+ // unlike RVN we DID consume the u32 nonce above — only the tail fields differ.
333+ let ( n_height, n_nonce_u64, mix_hash) =
334+ if ( version == KAWPOW_VERSION && reader. chain_variant ( ) . is_rvn ( ) ) || is_krgn_kawpow {
335+ ( Some ( reader. read ( ) ?) , Some ( reader. read ( ) ?) , Some ( reader. read ( ) ?) )
336+ } else {
337+ ( None , None , None )
338+ } ;
319339
320340 Ok ( BlockHeader {
321341 version,
@@ -2926,4 +2946,272 @@ mod tests {
29262946 let serialized = serialize ( & header) ;
29272947 assert_eq ! ( serialized. take( ) , header_bytes) ;
29282948 }
2949+
2950+ // ── Kerrigan (KRGN) multi-algo block header tests ────────────────────────
2951+ //
2952+ // Kerrigan is a Dash fork with PIVX-style Sapling. It mines four PoW
2953+ // algorithms concurrently; the algo id lives at `(version >> 9) & 0x7u32` and
2954+ // selects a different header tail per block. The four fixtures below are
2955+ // real mainnet headers fetched from electrum-kerrigan.jskitty.cat around
2956+ // the `blockchain.block.headers` failure point (height 22313+) reported
2957+ // against `ChainVariant::Standard`. See:
2958+ // https://github.com/GLEECBTC/coins/pull/1791
2959+
2960+ /// Fixture: KRGN algo 0 (X11), height 22312. Standard 80-byte BTC header.
2961+ /// Field layout: version | prev | merkle | time | bits | nonce.
2962+ const KRGN_X11_HEADER_22312 : & str = "\
2963+ 00000020\
2964+ cecedd36fc1337bd11435021f61d3659bb3878adda6c32bfe7d03039d3323cb8\
2965+ 9219536a41d8c300ee41722afa992ab97853c5793fa0f14965dafd62dd9cfaad\
2966+ 4714dd69\
2967+ 6eb8141a\
2968+ de037dfa";
2969+
2970+ /// Fixture: KRGN algo 1 (KawPoW), height 22316. 80-byte BTC prefix
2971+ /// (with the prefix u32 nonce zeroed on-wire) + 44-byte tail:
2972+ /// n_height (u32), n_nonce_64 (u64), mix_hash (H256).
2973+ const KRGN_KAWPOW_HEADER_22316 : & str = "\
2974+ 00020020\
2975+ 2fdf79031346fae14cec946b538edfcdfe7e862c362b479153051ea03c18fe08\
2976+ 83110049a44527184cfdb69384cc7f1ab5d79cc5385b97765e072431a708dbf1\
2977+ 3b17dd69\
2978+ 6ed4321b\
2979+ 00000000\
2980+ 2c570000\
2981+ c62f509228239162\
2982+ ec71a141c148176b53e852de723ec0a6fb8228ed9d18766bd554764a90cda7a4";
2983+
2984+ /// Fixture: KRGN algo 2 (Equihash(200,9)), height 22314. Zcash-style
2985+ /// header: hash_final_sapling_root before time, U32 bits, H256 nonce,
2986+ /// and a 1344-byte solution (compact-int length prefix `fd 40 05`).
2987+ const KRGN_EQUIHASH_200_HEADER_22314 : & str = "\
2988+ 00040020\
2989+ 030604f488da6a2b8eabb6bf2b6ce56d9934fdc6deef9bed8762feb8fe70a4ba\
2990+ 064f49dd64f3283ccf955b3f835e41a77c7d9f22556840a0a3df10196acb6f67\
2991+ 0000000000000000000000000000000000000000000000000000000000000000\
2992+ 9316dd69\
2993+ 2963021d\
2994+ 6000000a00000000000000000000000000003e0000000000000000000000df5c\
2995+ fd4005\
2996+ 0077d885da4dfa9b1378024dcca2ee91e8e6bcee7609862bec42a6697940b452\
2997+ 85642e26d967d9794f7f158cbf7ca885e464b7eb71d286c9d4c1351892fefe36\
2998+ 66979381d59cc4dd5ac3eb77c884a285fb778d7a229e19793e0928398732a2cd\
2999+ 7d50fb54bb99115a832ee44aa973a3bee3755eb3b031323b72e99d3ed86b2772\
3000+ 5ba395d144d34525998af7f7238765cd7cf9d84f436da305566d6be85ac89e07\
3001+ fe135360157df72d0351e0b07acdaaa322aab11ab2dc29a1bdc07adbbf0c2a35\
3002+ fb40467d408a99b1528157437ea71c1ac7b408f6ad0a8b2dfa578d11b15e0135\
3003+ 4aae8971354e682ba7b99433ef2e71c9bef3139b764777328b7fc34e0981bfd9\
3004+ dd9cd40b2ac2e1415c57cbe920dfbd41660ebdda9c17cdea44f4d44480ceef7b\
3005+ 1739017c52d60db157d6eae8130dbd5fb5b4b2511f2af7243d0f335c92b71520\
3006+ 99601988ea5770dcf8f735f47614aa8700f31597cfc7477e55786363aa59163a\
3007+ 77db3fa8601c689d5a13f6f92bf1f284303ff6604a54025624d018aa7f917087\
3008+ 7fa7a6b6b4273e7d4c8e7012bcd876386e3be8701ca8932566d438616e490d38\
3009+ 148b712d04dcf20383488db92044e1c991dac4e5960fbe97bc26f90b276d537f\
3010+ d5a0918810df79f83a0e6d97c07b096ef84dcc5b55fbe064d5ca92dc807a09ff\
3011+ 18acb91256be90935da8bf567da2432d65120330fafd175a032c3ead5214e42f\
3012+ 7dc1733cd0dfe11a9ba496e099042408de85c7c131499541724fd93da2d9bdbb\
3013+ d1a015d926982d92a35913f7c2fd83e35841dff5b0e68017d955c2a6149a07da\
3014+ df33e443fb1d86cf5319b41d051c0713a14d79a5525ea3d48027b8c5c165dc34\
3015+ f305d7373554c61e5ae5dbf3b2c9fe3291552c9ea95308d963b7799aefc5afff\
3016+ 20eee3b3cc48f5c60ca1763df616bc3a22bcb5a22d14598dcb249d74707be32e\
3017+ 0181ccfa41c4e592763643310b2e5b4f111739939e14ee61b213ceb47f3b4401\
3018+ d72f5d442aeecfdbb3290756ad2f878d8101e86692dc07af71aa57aad555fb0d\
3019+ c60ebc8eafe7f7ac0a33791f7ea63124a3d70ed707510c66f3c3668d80b615ae\
3020+ 4dcab935c0e6b170780df7d0ba7c3075239eeb3272219a3aad85021a32e60af6\
3021+ d9faff4a0de5b8cfa3fe6da1b43daf127e40a334b7d794890e6f5345e73a1967\
3022+ 62e3f6f4bc9a3f05079e617e35c1edcf672d07edc3d885221f31fc0ef10ad7a0\
3023+ fa738fc3dcfec4428b8e2439f2083972b276113caeeba98a486ac029f29e67be\
3024+ d7929d4abc43a235276e4afe4ead6b6766f390bc7e4c2dc82a7b773f22a4acc6\
3025+ a1dc123ffcf13427826e2b61c8ad157eb24790461ad55e73035c32b7f30fe9c3\
3026+ 3e2a33df33442ac4ae20099fac634dba547a16ac95fa2b74db9be03c9c1796cf\
3027+ 670ffd83d6f4b783d9e1de2e62f9559d01a2452717905aac86a2a57a76bd1f45\
3028+ e0f95e118d136627bd21ab79f5e6d865029cf7c0296338b6f75620c2b3d19da0\
3029+ 4fa35fd44246b8ea55a99e983eaa443bd54ba1bd62138b9534a410882d92cd87\
3030+ 105bf8830576f56a99cf935cdd971236fe72ab36ceedfa77160669c957bf19ae\
3031+ f9a92697cb30f31a0eefe01cba660881b3fc7943acbab77bb501cf3b75a944da\
3032+ 9d9d5a118b9cb4e484fa610453e1979dc706a947c48a712005ad370e658a5e29\
3033+ 7e6bb48d8c57763d752a4e2f9f86e9aec12af5e01dbd720afd9a5faf6b243cf9\
3034+ 8eac2d3d6dfdfa1a690b8eb6e3e00546a3f20982f37f5b33bf745286721da9a7\
3035+ 20f4d1d15c915158733d9e7909b66a33d98731cdd41c22f5f8d3efb4bf062e16\
3036+ 1915f0036e758d43fd570144afa6dd6d4661a739d4980bd43f9c780f20579b30\
3037+ b22c8eb9e8a5d817ba6df50fd77609e18789e1f7a6b4ce87da0ba156e3f9b63c";
3038+
3039+ /// Fixture: KRGN algo 3 (Equihash(192,7)), height 22313. Same Zcash-style
3040+ /// layout as Equihash(200,9) but with a shorter 400-byte solution.
3041+ const KRGN_EQUIHASH_192_HEADER_22313 : & str = "\
3042+ 00060020\
3043+ 864a47125d16e571501cad908ca8d7f8c824e1414d8178079809000000000000\
3044+ 41a42ad52ba9c797d4bdbec73f6283015a474735b868e46d6ce45426cb92dea4\
3045+ 0000000000000000000000000000000000000000000000000000000000000000\
3046+ 6815dd69\
3047+ e446011e\
3048+ c02cf51819836be40135d5f8728995f74d3d0b930000000000000000d1103020\
3049+ fd9001\
3050+ 01dab64d2b6fe3b0dfdfc4f7601e523461d3d4bd2b7da4bbf63a45ffda27052d\
3051+ 6adcd7d8ac382a3ebe370d725b7823ceb7a91c2d1ce6882056e01e7ac68db692\
3052+ a5a4a26ef6fed979b48d1a4e5e78f09d65d51f4c902c35f6ca42cf5d78c1d733\
3053+ 0524185614df0f78d3e6df53569a4d1aa35e7306f84fd5665ce2c69664439dea\
3054+ a737de204372d1aa1a9479bba4d1d4ddaed13f4e03521c59067b361be41cdf74\
3055+ 4652b26a0b3d4bbfe2021e9315a7a0494b25e493425eb4435a8b0e755569f42c\
3056+ 1f016eae9fd7abdd08c4b79b4f4b02520c9d7d9de1b3889df5f5c28185bd9ff3\
3057+ 4c2fc9ef9a519fcd84115b601fb36197dfa99693951cf3dbd82910d693790746\
3058+ 62fc783a9fd7744fe8a39ee8f91bbaab5cef7e2d6eccad449d1942f43851b1d6\
3059+ df5e33e68146e83f55d9610d09ca6968f4b04af30a8f9fd3d0e3a3dcabef1272\
3060+ bbdbd44b30148edca805232c442c7cdfd8144b66af220b99dc954beb86f71e54\
3061+ 10c0b1ec4e4508790b3c4383a3ccf782394249d323944f475de549f04f9dcf06\
3062+ b202e9f63713ae6af7663149edc27da2";
3063+
3064+ #[ test]
3065+ fn test_krgn_chain_variant_try_from ( ) {
3066+ use std:: convert:: TryFrom ;
3067+ let cv = ChainVariant :: try_from ( "KRGN" ) . expect ( "KRGN variant must be recognised" ) ;
3068+ assert ! ( cv. is_krgn( ) ) ;
3069+ assert ! ( !cv. is_btc( ) && !cv. is_rvn( ) && !cv. is_pivx( ) ) ;
3070+ }
3071+
3072+ #[ test]
3073+ fn test_krgn_x11_header_round_trip ( ) {
3074+ let bytes: Vec < u8 > = KRGN_X11_HEADER_22312 . from_hex ( ) . unwrap ( ) ;
3075+ let mut reader = Reader :: new_with_chain_variant ( bytes. as_slice ( ) , ChainVariant :: KRGN ) ;
3076+ let header: BlockHeader = reader. read ( ) . unwrap ( ) ;
3077+
3078+ assert_eq ! ( header. version, 0x20000000 ) ;
3079+ assert_eq ! ( header. time, 1_776_096_327 ) ;
3080+ assert ! ( matches!( header. bits, BlockHeaderBits :: Compact ( _) ) ) ;
3081+ assert_eq ! ( header. nonce, BlockHeaderNonce :: U32 ( 0xfa7d03de ) ) ;
3082+ assert ! ( header. solution. is_none( ) , "X11 headers have no Equihash solution" ) ;
3083+ assert ! ( header. n_height. is_none( ) , "X11 headers have no KawPoW tail" ) ;
3084+ assert ! ( header. hash_final_sapling_root. is_none( ) ) ;
3085+
3086+ let serialized = serialize ( & header) ;
3087+ assert_eq ! ( serialized. take( ) , bytes, "round-trip must be byte-exact" ) ;
3088+ }
3089+
3090+ #[ test]
3091+ fn test_krgn_kawpow_header_round_trip ( ) {
3092+ let bytes: Vec < u8 > = KRGN_KAWPOW_HEADER_22316 . from_hex ( ) . unwrap ( ) ;
3093+ assert_eq ! ( bytes. len( ) , 124 , "KRGN KawPoW headers are exactly 124 bytes" ) ;
3094+
3095+ let mut reader = Reader :: new_with_chain_variant ( bytes. as_slice ( ) , ChainVariant :: KRGN ) ;
3096+ let header: BlockHeader = reader. read ( ) . unwrap ( ) ;
3097+
3098+ assert_eq ! ( header. version, 0x20000200 ) ;
3099+ assert_eq ! ( ( header. version >> 9 ) & 0x7 , 1 , "version encodes algo=1 (KawPoW)" ) ;
3100+ // Kerrigan keeps the 80-byte prefix u32 nonce on the wire (zeroed for KawPoW);
3101+ // this is the one observable difference from RVN KawPoW which omits it.
3102+ assert_eq ! ( header. nonce, BlockHeaderNonce :: U32 ( 0 ) ) ;
3103+ // KawPoW tail: n_height, n_nonce_64, mix_hash.
3104+ assert_eq ! ( header. n_height, Some ( 22316 ) ) ;
3105+ assert_eq ! ( header. n_nonce_u64, Some ( 0x6291_2328_9250_2fc6 ) ) ;
3106+ assert ! ( header. mix_hash. is_some( ) ) ;
3107+ assert ! ( header. solution. is_none( ) ) ;
3108+
3109+ let serialized = serialize ( & header) ;
3110+ assert_eq ! ( serialized. take( ) , bytes, "round-trip must be byte-exact" ) ;
3111+ }
3112+
3113+ #[ test]
3114+ fn test_krgn_equihash_200_header_round_trip ( ) {
3115+ let bytes: Vec < u8 > = KRGN_EQUIHASH_200_HEADER_22314 . from_hex ( ) . unwrap ( ) ;
3116+ assert_eq ! ( bytes. len( ) , 1487 , "KRGN Equihash(200,9) headers are 1487 bytes" ) ;
3117+
3118+ let mut reader = Reader :: new_with_chain_variant ( bytes. as_slice ( ) , ChainVariant :: KRGN ) ;
3119+ let header: BlockHeader = reader. read ( ) . unwrap ( ) ;
3120+
3121+ assert_eq ! ( header. version, 0x20000400 ) ;
3122+ assert_eq ! (
3123+ ( header. version >> 9 ) & 0x7 ,
3124+ 2 ,
3125+ "version encodes algo=2 (Equihash 200,9)"
3126+ ) ;
3127+ assert ! ( matches!( header. bits, BlockHeaderBits :: U32 ( _) ) , "Equihash uses U32 bits" ) ;
3128+ assert ! (
3129+ matches!( header. nonce, BlockHeaderNonce :: H256 ( _) ) ,
3130+ "Equihash uses H256 nonce"
3131+ ) ;
3132+ assert ! (
3133+ header. hash_final_sapling_root. is_some( ) ,
3134+ "Equihash has hashReserved / sapling root"
3135+ ) ;
3136+ let sol = header. solution . as_ref ( ) . expect ( "Equihash must carry a solution" ) ;
3137+ assert_eq ! ( sol. len( ) , 1344 , "Equihash(200,9) solutions are 1344 bytes" ) ;
3138+ assert ! ( header. n_height. is_none( ) , "Equihash headers have no KawPoW tail" ) ;
3139+
3140+ let serialized = serialize ( & header) ;
3141+ assert_eq ! ( serialized. take( ) , bytes, "round-trip must be byte-exact" ) ;
3142+ }
3143+
3144+ #[ test]
3145+ fn test_krgn_equihash_192_header_round_trip ( ) {
3146+ let bytes: Vec < u8 > = KRGN_EQUIHASH_192_HEADER_22313 . from_hex ( ) . unwrap ( ) ;
3147+ assert_eq ! ( bytes. len( ) , 543 , "KRGN Equihash(192,7) headers are 543 bytes" ) ;
3148+
3149+ let mut reader = Reader :: new_with_chain_variant ( bytes. as_slice ( ) , ChainVariant :: KRGN ) ;
3150+ let header: BlockHeader = reader. read ( ) . unwrap ( ) ;
3151+
3152+ assert_eq ! ( header. version, 0x20000600 ) ;
3153+ assert_eq ! (
3154+ ( header. version >> 9 ) & 0x7 ,
3155+ 3 ,
3156+ "version encodes algo=3 (Equihash 192,7)"
3157+ ) ;
3158+ assert ! ( matches!( header. bits, BlockHeaderBits :: U32 ( _) ) ) ;
3159+ assert ! ( matches!( header. nonce, BlockHeaderNonce :: H256 ( _) ) ) ;
3160+ let sol = header. solution . as_ref ( ) . expect ( "Equihash must carry a solution" ) ;
3161+ assert_eq ! ( sol. len( ) , 400 , "Equihash(192,7) solutions are 400 bytes" ) ;
3162+
3163+ let serialized = serialize ( & header) ;
3164+ assert_eq ! ( serialized. take( ) , bytes, "round-trip must be byte-exact" ) ;
3165+ }
3166+
3167+ /// Reproduces the `blockchain.block.headers` batch parse that was failing
3168+ /// with `MalformedData` on `ChainVariant::Standard`. Concatenates one header
3169+ /// of each of the four Kerrigan algos in a single buffer and verifies the
3170+ /// reader consumes them sequentially, producing the right field shape for
3171+ /// each — which is exactly how an Electrum batch response is laid out.
3172+ #[ test]
3173+ fn test_krgn_mixed_algo_header_batch ( ) {
3174+ let mut buf: Vec < u8 > = Vec :: new ( ) ;
3175+ for hex in [
3176+ KRGN_X11_HEADER_22312 ,
3177+ KRGN_KAWPOW_HEADER_22316 ,
3178+ KRGN_EQUIHASH_200_HEADER_22314 ,
3179+ KRGN_EQUIHASH_192_HEADER_22313 ,
3180+ ] {
3181+ buf. extend_from_slice ( & hex. from_hex :: < Vec < u8 > > ( ) . unwrap ( ) ) ;
3182+ }
3183+ let total_len = buf. len ( ) ;
3184+
3185+ let mut reader = Reader :: new_with_chain_variant ( buf. as_slice ( ) , ChainVariant :: KRGN ) ;
3186+ let mut parsed: Vec < BlockHeader > = Vec :: new ( ) ;
3187+ while !reader. is_finished ( ) {
3188+ parsed. push ( reader. read ( ) . expect ( "batch parse must not fail on KRGN" ) ) ;
3189+ }
3190+ assert_eq ! ( parsed. len( ) , 4 , "exactly four headers in the fixture batch" ) ;
3191+
3192+ // Each parsed header should have the algo-specific field signature that
3193+ // matches its version bits — i.e. the deserializer actually dispatched
3194+ // correctly rather than silently skipping bytes.
3195+ assert_eq ! ( ( parsed[ 0 ] . version >> 9 ) & 0x7 , 0 ) ; // X11
3196+ assert ! ( parsed[ 0 ] . solution. is_none( ) ) ;
3197+ assert ! ( parsed[ 0 ] . n_height. is_none( ) ) ;
3198+
3199+ assert_eq ! ( ( parsed[ 1 ] . version >> 9 ) & 0x7 , 1 ) ; // KawPoW
3200+ assert ! ( parsed[ 1 ] . n_height. is_some( ) ) ;
3201+ assert ! ( parsed[ 1 ] . solution. is_none( ) ) ;
3202+
3203+ assert_eq ! ( ( parsed[ 2 ] . version >> 9 ) & 0x7 , 2 ) ; // Equihash(200,9)
3204+ assert_eq ! ( parsed[ 2 ] . solution. as_ref( ) . unwrap( ) . len( ) , 1344 ) ;
3205+
3206+ assert_eq ! ( ( parsed[ 3 ] . version >> 9 ) & 0x7 , 3 ) ; // Equihash(192,7)
3207+ assert_eq ! ( parsed[ 3 ] . solution. as_ref( ) . unwrap( ) . len( ) , 400 ) ;
3208+
3209+ // Re-serialize the whole batch and verify it matches the original buffer
3210+ // byte-for-byte: this is the strongest round-trip guarantee we can give.
3211+ let mut round_trip: Vec < u8 > = Vec :: with_capacity ( total_len) ;
3212+ for h in & parsed {
3213+ round_trip. extend_from_slice ( & serialize ( h) . take ( ) ) ;
3214+ }
3215+ assert_eq ! ( round_trip, buf, "full batch round-trip must be byte-exact" ) ;
3216+ }
29293217}
0 commit comments