From ddd5441227b82daf9f9fba34159939b17ab39951 Mon Sep 17 00:00:00 2001 From: David Case Date: Thu, 17 Jul 2025 14:08:03 -0400 Subject: [PATCH 1/9] Fix BEEF validation stability issue - Add transactions with merkle paths to valid set before validation - Use iterative validation for proper dependency ordering - Ensure deterministic validation results Fixes #211 --- transaction/beef.go | 56 ++++++++++++++++++----------- transaction/beef_test.go | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/transaction/beef.go b/transaction/beef.go index 4b505d66..c5d3e6de 100644 --- a/transaction/beef.go +++ b/transaction/beef.go @@ -803,23 +803,18 @@ func (b *Beef) Verify(ctx context.Context, chainTracker chaintracker.ChainTracke return true, nil } -// SortTxs sorts the transactions in the BEEF by dependency order. -func (b *Beef) SortTxs() struct { +type SortResult struct { MissingInputs []string NotValid []string Valid []string WithMissingInputs []string TxidOnly []string -} { - type sortResult struct { - MissingInputs []string - NotValid []string - Valid []string - WithMissingInputs []string - TxidOnly []string - } +} - res := sortResult{} +// SortTxs sorts the transactions in the BEEF by dependency order. +func (b *Beef) SortTxs() SortResult { + + res := SortResult{} // Collect all transactions into a slice for sorting and keep track of which txid is valid allTxs := make([]*BeefTx, 0, len(b.Transactions)) @@ -891,13 +886,7 @@ func (b *Beef) SortTxs() struct { for k := range missing { res.MissingInputs = append(res.MissingInputs, k) } - return struct { - MissingInputs []string - NotValid []string - Valid []string - WithMissingInputs []string - TxidOnly []string - }(res) + return res } func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { @@ -941,15 +930,40 @@ func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { } } + // Single pass: add transactions with merkle paths to valid set and collect those needing validation + remaining := make(map[string]*BeefTx) for txid, beefTx := range b.Transactions { - if beefTx.DataFormat != TxIDOnly && beefTx.Transaction.MerklePath == nil { + if beefTx.Transaction != nil && beefTx.Transaction.MerklePath != nil { + txids[txid] = true + } else if beefTx.DataFormat != TxIDOnly && beefTx.Transaction != nil { + remaining[txid] = beefTx + } + } + + // Keep processing until we've validated all transactions or can't make progress + for len(remaining) > 0 { + progress := false + for txid, beefTx := range remaining { + // Check if all inputs are valid + allInputsValid := true for _, in := range beefTx.Transaction.Inputs { if !txids[in.SourceTXID.String()] { - return r + allInputsValid = false + break } } + + if allInputsValid { + txids[txid] = true + delete(remaining, txid) + progress = true + } + } + + // If we didn't make progress, the remaining transactions have missing inputs + if !progress { + return r } - txids[txid] = true } r.valid = true diff --git a/transaction/beef_test.go b/transaction/beef_test.go index 39caf246..2e240425 100644 --- a/transaction/beef_test.go +++ b/transaction/beef_test.go @@ -1259,3 +1259,81 @@ func TestMakeTxidOnlyAndBytes(t *testing.T) { require.NoError(t, err) _ = beef.ToLogString() } + +func TestBeefVerify(t *testing.T) { + const iterations = 1000 + + tests := map[string]struct { + hex string + }{ + // the following [not mined] beefs contain transactions that don't have BUMPS + // e.g.: (not mined tx) -> (not mined tx) -> (mined tx) + "beef v2 from testvectors one-in-one-out [not mined]": { + hex: "0200beef01fde80301010000f6282a580ebf0cebd3edbb4ac129d2d7f8a1b337ab642f70377f3d9040eca1d102010001000000012e3f4683e173b40a20527fe5719633ba070df649983614886e90e45aecf2ac56000000006b483045022100c7ddc5159fc630d28f4beeeafa73bc8d32f25b01909732d8d44b9cdbbc85888502206a0a6269bc47c633441a7b5aff120fd0760024badd660f24f713889c0ee70ecb4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff01a2860100000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac00000000000100000001f6282a580ebf0cebd3edbb4ac129d2d7f8a1b337ab642f70377f3d9040eca1d1000000006a4730440220291e6769c2383c82fd3c06de833589d9401dbb55838bdc02a76d8d7a98d3cac302207ad2de40877eab59981f2d46dba1cdefd40846db840ae24094eb07688b3e4ee64121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff01a0860100000000001976a9143cf53c49c322d9d811728182939aee2dca087f9888ac00000000", + }, + "beef v1 from mainnet tx [not mined]": { + hex: "0100beef02fe22d10d000a04fd4c02021f0ade7298c9ee505c4ce728eff01b8ee5afebfd6bf78fc055de007adc1b9045fd4d020090bc5a08cf51172b344a5ffbe3ff7be0257a7ac1ffe635d2a2e4b3c609580594fd5602024d985cfd5e069fabc0af08d61d29dc7bb73e4ce2d7fe5f67078e224330b7eb04fd570200c73c2513231ea9f077fc213f38b69ce6ddc6a547ee7f5a8fef05433036add1ac02fd27010029634f4fdbad28cbb98b69df1526b0a6fb8003d301eee886ce7b50721af06185fd2a0100524ca2123d76c175e7775c63558f8c9559e75c12be22192ba3f01ad69251d39102920001752ececf0498cd217680ffa35036213ec65192e547a82fd87dda64745bc57c9400ed1c963033420dd37b50a52b0106fce7f3b17797328ad06197ba14b3c02406510248000773709f08fb89f4dd3f6c1ecdc533e57956d684d123f55d0a1c2fa81d497e5c4b00e6fa5029ff33b5039dcf1ad29d48ec31a3f1280e05228a09a45fff89773117c30224008d830b0871e68181c11fa55bcfc7b4ba4cfe7c141c83e5572c241e16b5501c4925002de377765a914b52423169c000e6f01e022a6d82c624eec484d7de37bf98cea8011300032584497fb4c4d774f0e5b9f60cfe71020dfa4d62be03004c43c27b93c4b1e5010800b738e56ae3afbd6b9ea3a5f2ee28105197a5b047ea90c09af8388a743faef7e50105006c9ab3fde287ab528744668c0d472bb20b8408af403cc17155d8d5770b1a758e0103009bfb3e063448a27874885f3914a52f4793c38cfc754856008b47a5ba944de7d0010000b2ed8e6765ee98e38dd452855c7e36b2be0a077e476d717cd7166b15acc87f5efecab20d000a02fd2b02022412eba9148402370dcd9bdebc2335684fa843452f9c903a1926056c2ded9e9dfd2a0200912b4cfa67a29ad0566906d45438f7595e891675a05f196510a70bd7a78978b501fd1401002444c9b082f3b799b43deb4759df3222edb0d318ee658059183bb0ebf7f94a76018b004bcccd23218a409e11f721b2fed87c2da39291b56187e19ab8c588433a4f7364014400c183687dac2384ab0c1578fb458b9c00329614c8bd75a330045375cab2b014a10123009ea881f36d2698031183fc7ddba5577060fac586d9938166b7a2a7886e2521fa011000e35f0f6da83643cdd837d6f4aed1975a1dd0f8f79a4879c56c189645f7dd003a010900ae7f1a9ef7f64782d6a0f09beefc74f03627730992491f2529cc3e03adea221b01050054a684e36d6da791b60ef667425b94656e61c97db48fe6319686e59fb17e3ff40103008e5cfeadb07f2471590fea982cf7b8c6e113cc4cda99e330e517be3bde55db8d010000a022a5a0fbd1be17f9690056818d7308002b62d2b60d1ecd99014ddda5af6bdf050100000002a3513ec0e99df0307459aeabb3ec2447f680543375be279f981d9ab74ff0564e000000006b48304502210093d7862f1b8adefa47cd53c383721beebcd0691c889069d70435a7eef3f5f8e002205d5cd9323670b2e97b5d6b9d7506c7e8e518b35401c432cc00727ba0ece27eca4121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff6cf9e14113fe68992d84a2beba57307dc6db17cd5f6c6776ac3d28abdb60054f010000006a47304402205c58c456d527074320ad7d1ee383e3fc2f8423c39e5125161bb9e7ee8b330b7d02206e161f172b8d6ddfde08a9f0607cc224adc1919d510d52daf42868ccc3e808b04121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff020c000000000000001976a9147072e2ef390050bc43726d487c117f96da9c534b88ac7a6f0100000000001976a9147072e2ef390050bc43726d487c117f96da9c534b88ac0000000001000100000002ecdbe1d3168f8ac58c0c9a723700de27671132dd532bda051b2b67f53815fa4d000000006b483045022100d9a8278ed88a26b73b2e7a32267572c2c3a5f1692fa3aa2874b4b420f3c6aadf02205ec100c4870a0c8920898554b3f17727b7a60c68ec5bfdc3b17f29a2e449b6854121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffffa3513ec0e99df0307459aeabb3ec2447f680543375be279f981d9ab74ff0564e010000006b483045022100c4833dc2da31901e50394d493d6f4c2863eb81c2e671c3e17cfa815fc3255199022043029192ed27a3527d8bb60c9c927ac88c8bfa9f8d2f1cd8d789e554ec8e36bf4121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff020d000000000000001976a9147072e2ef390050bc43726d487c117f96da9c534b88ac27690100000000001976a9147072e2ef390050bc43726d487c117f96da9c534b88ac00000000010001000000021f0ade7298c9ee505c4ce728eff01b8ee5afebfd6bf78fc055de007adc1b9045000000006b483045022100a44a9236494e95be20a9e7bc96e14bb7bef03e2ce33f338f0081ca99484d929a0220787494efdf7ebd7790599f06ffbb21fb906c6b47c89f180ec8ca62f145e71f484121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff4d985cfd5e069fabc0af08d61d29dc7bb73e4ce2d7fe5f67078e224330b7eb04010000006a47304402203efba1a8cf538998a0975949898e42d2c69df36561969c5a29b38cf33511e9580220614f98ddaa9d5bb05fad38010764332964ec1b41f967861679e390236a4efe814121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff020d000000000000001976a9147072e2ef390050bc43726d487c117f96da9c534b88ac706f0100000000001976a9147072e2ef390050bc43726d487c117f96da9c534b88ac00000000000100000007720e0554f291086056263b0e0b43d482bd28cb62a63d61d9729d15795cccebd600000000b24730440220197cd052433a71be1b6c31d9ae7807b65e7e90118b789ec30226f0c703e4327d02201ca938d1c3c3831552ed2b0df55deb1eac0bbeb0ef856c2cd5d52a7058ec69854147304402206abca1efc5513bc7e7bea68f033378db2d7399fc2bee0caac97104ba2cad4f950220339053f1aca658ceb45ca43abb941041013af13fc89051aae47a4e59c61bd20dc12102bd45e58523dfc46c2ef3ee325802d324e30a193cd83271e4e2142989626ccefaffffffff8a35b1e15447fcaa5cce7c85431705f009c726b9e7ca90c0a054b8c884cb1b3c00000000b2473044022036590d5105abfcf19e7f19f78e40a844bf3cb8c68588450a4af80ab18502663602202a8e2193a44a37ff66853b8fa499e55098f3c08e36d0cd68e3ab4224c5d778f14147304402204edc61ce6ebbe3426f36fb02e3c6ca9f34ab355a26751d5180b10140b49d925b02206b438187b6d1a539699c66259f74815c2535b646e659960d28fc1f498b10a022c12102bd45e58523dfc46c2ef3ee325802d324e30a193cd83271e4e2142989626ccefaffffffff319787bdc7b90a3a607f86f44c452944e778abbe8ee6761a82b3ae4024e6795702000000b4483045022100e3598caa01b47ea6ca5e8b186f3fc647eeda32ea97ff77719be76cda9504143a02200c996409d852460dd6c371f53603327e8a4ccb39bcf9c34da41ac02116d4e84c41483045022100e7c6e466b5eb1f79eae6d896de2b0ddbda189387e35ccbc9969f43cee416e6730220071dc62f847857973dea94eaaa4226ba39b07e09422cb9f2ce81e3d21572f0f7c12102bd45e58523dfc46c2ef3ee325802d324e30a193cd83271e4e2142989626ccefaffffffff3def520b9880fc97a032e84ce3381da111588e97d6f536a72a6b9aeb3375297302000000b2473044022017700a6811f0db93143a8d9cd92ec320affa762a29d75270a6fcffd1b29183de0220595092b981da29b66e823899010dfe07a4eb53e8eb4ec1b8ee4139ece56b7e6441473044022015b6355e7640d54fb72d3ad93a0d76691fe96cbd589ff6a9196e327d9c41aa4702204ef00e1ad3d133e73ee53d2dc2208f931eabcee519c30419ffdd603f8a56d289c12102bd45e58523dfc46c2ef3ee325802d324e30a193cd83271e4e2142989626ccefaffffffff4df52d1c36b5794a82cd01046fad0ba9e34456b50267bb0e4372c753e8cf08ea00000000b3473044022055cc1b09e7dcdb76344cf2120c60792578f3518e62ecef5fe9ea8fb117338781022058d0ec4741cb9a5380b0478df068333ed45e4f3cb25bc16d9162b683020eb04041483045022100bf49323c19f8d2283a31f8047b07cac04237096d15df014f85661460dbf4c01802205863c8f6935b202e2647a1e6ec45f367cd298bd61ce4c7c3c1c6be738eb6b038c12102bd45e58523dfc46c2ef3ee325802d324e30a193cd83271e4e2142989626ccefaffffffff801c15e3596aeb2859953b4b412a2737944d7d3177ed3d34e2546dac4d50201302000000b2473044022006210e58b3e206f14f2a4dd9536d837b00271b695210f1a1e8a2047d71b4b85402202a6ad9f4ee00dcc99c421e257e46500a19110e319601566403241798e507954b4147304402204e5e10917378c0b4225ba7f5a317bc670359d40afdb1e7e8dd8247928770524202207b77f8e3eb534cba7413c465f9e7d83922478b44e8bf9e62ea726ac807b08647c12102bd45e58523dfc46c2ef3ee325802d324e30a193cd83271e4e2142989626ccefaffffffff912b4cfa67a29ad0566906d45438f7595e891675a05f196510a70bd7a78978b5000000006b483045022100f950eef70d59afd91e988dbb2fa9e620c508a2a71ecc4044ea73981e27dc055302200b30bb9705be23f8bc2fba08c201caf77609abc0e9bb4e42e0826e1809de05504121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff030100000000000000d20063036f726451126170706c69636174696f6e2f6273762d3230004c787b2270223a226273762d3230222c226f70223a227472616e73666572222c226964223a22616535396633623839386563363161636264623663633761323435666162656465643063303934626630343666333532303661336165633630656638383132375f30222c22616d74223a2232383730383133227d6876a914b5ff6c546a60342e88e5ebe7dad51a24143383f588ad21020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fac0100000000000000cf0063036f726451126170706c69636174696f6e2f6273762d3230004c757b2270223a226273762d3230222c226f70223a227472616e73666572222c226964223a22616535396633623839386563363161636264623663633761323435666162656465643063303934626630343666333532303661336165633630656638383132375f30222c22616d74223a2231303030227d6876a9145d34be178f0bc32c3d85671427f1e70694ca8a3b88ad21020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fac0100000000000000d30063036f726451126170706c69636174696f6e2f6273762d3230004c797b2270223a226273762d3230222c226f70223a227472616e73666572222c226964223a22616535396633623839386563363161636264623663633761323435666162656465643063303934626630343666333532303661336165633630656638383132375f30222c22616d74223a223137373431363233227d6876a914a5854b1a82f5c71b664a19b64c358f54d6acb18c88ad21020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fac00000000010101000000022412eba9148402370dcd9bdebc2335684fa843452f9c903a1926056c2ded9e9d00000000b3473044022017c67b7d2ec56df57643b97855cbde504772b45b5aa3d3f2f70543d0c7f640e10220102b8fce1bc9e0fa7b633119baae429522ffc08de063770c78a007cb7ab1d2d241483045022100a85692c4ba3828b0f12b6d5c36ff5fffb3e6a0f8a0684ebc59d925c75a64d91c0220776ad270f133ce0f5fc28b8d7ac8dcc6236304346d909ccbb8db3dddb910571bc121036823f82f6c9c279b17c6e5edb0de192a9757778ef978112a62c9a1d17efa4ebaffffffffb05957c4cf6e745f2e147610575f4ba632a84032c86862dec0c656db0ba37911000000006a47304402203bb4c3d0fcae2c72fa6d88f4045447fd8fa2e33afb5867d4092923fa872af67802204faece6a38513d97440c31169a6fc6d69fb4766b4ad9e905218996179c7441f44121020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fffffffff030100000000000000d20063036f726451126170706c69636174696f6e2f6273762d3230004c787b2270223a226273762d3230222c226f70223a227472616e73666572222c226964223a22616535396633623839386563363161636264623663633761323435666162656465643063303934626630343666333532303661336165633630656638383132375f30222c22616d74223a2231303030303030227d6876a914a5854b1a82f5c71b664a19b64c358f54d6acb18c88ad21020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fac0100000000000000ce0063036f726451126170706c69636174696f6e2f6273762d3230004c747b2270223a226273762d3230222c226f70223a227472616e73666572222c226964223a22616535396633623839386563363161636264623663633761323435666162656465643063303934626630343666333532303661336165633630656638383132375f30222c22616d74223a22313030227d6876a9145d34be178f0bc32c3d85671427f1e70694ca8a3b88ad21020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fac0100000000000000d20063036f726451126170706c69636174696f6e2f6273762d3230004c787b2270223a226273762d3230222c226f70223a227472616e73666572222c226964223a22616535396633623839386563363161636264623663633761323435666162656465643063303934626630343666333532303661336165633630656638383132375f30222c22616d74223a2231383730373133227d6876a914b5ff6c546a60342e88e5ebe7dad51a24143383f588ad21020a177d6a5e6f3a8689acd2e313bd1cf0dcf5a243d1cc67b7218602aee9e04b2fac0000000000", + }, + + // the following [mined] beefs contain transactions that all have BUMPS + "beef v1 from actual testnet tx [mined]": { + hex: "0100beef01fe849e19000c02fdcf0a02c8c06c5fac63510b2b02ccab974a6ef0b0a4910dd8e881c06964f2b52d7ff415fdce0a00ac05565e579d8c4257313d90ce7bea754aa41add817a0616a9199c84a89d273301fd66050072958bee9c51d1a7511759ef6c73aa03a0533749e887c06514504c466a185fc201fdb202004d106b759b760b423b05be8e53b7ccd44db1cf8c39fd609ee70c316b0a2964df01fd5801003521b209685ae64f5a7f41fcfc5d487fe1b0162ee5a311620c252f6f48714ad101ad000af15dea439d12d3330dc65b5fac8a8e786d80c9f4e8c10dec91807c2fa085380157000e5a9d088abcc6bfb57f6aeb7f12a4fbe63fa07477bef78fa87f967466c374d4012a0083c8207772fb8586071053e855af973a2cce232d45d6a90e3ad403015a003ad1011400074cd69e726d1f7b9f7f1f301f701eef3dd36cbec654ddf7ee897d2567fbc2d4010b00620e7f9bd848d9123aad73e7b28b05e830eaf7d7188f3322d79b2256934026f9010400bbb0cac6a484ac94f774b0c795fa9f116f8251e7cdfe7b374938b8806563f383010300a7074e5aa1e7ffc5754fb762c7853adc20a80c25fad6ad60ddbf80b6d8ad02e40100009c956d581a811c8d45a81b716fbb9c4349c4be011f24c44d236a493b24ca5f4201010000000122ffae11e662c209b8cc5ecce312af425b06f44668d667bb8b09fc04f1e25653010000006b483045022100bac3cea0816c2c8863b6a5207bef9c2236716c58140448d980241f851705872f02201a912284e254e76f33fab9b82eb577921950e9091edf4e79d9a6d00453a23a4e41210231c72ef229534d40d08af5b9a586b619d0b2ee2ace2874339c9cbcc4a79281c0ffffffff0201000000000000001976a914d430654b50459aa04e308c07daf4871185efdc3088ac0d000000000000001976a914cd5ea7065a42329a574b1eb7af9fbbca8a94e44b88ac000000000100", + }, + "beef v1 from actual mainnet tx [mined]": { + hex: "0100beef01feb2d20d000402070275108dbcbc9b210d852d90e79f17feebb5ac99640b23055a33daf6aa1860fa670600aaccf30ae2fc73c7ed85601ce3aff36361b8a197e1a4860da4288b452d903a4a010200d1e94851f84a3aa5c2d893c386607471b67f2eec27f8f116c1e05197e84d0c6b010000d87dca62e921e12ed2bc69fc988a922065fa38ae6ad62b07417bfd3e9f74697101010051bb5ce45a8c5e4101246e1f48ae969ef6fc337000fd9dfa4db39e2e114c85af010100000001dc3a61bb7feca472a600ac66b1cbea3e119500853b6aa4f6839cae84e0c3c862010000006b48304502210085bd20643d927b9c505b2bd27c84f447c7407bcdca8d727510d9181a39d71a6f02204210132e103a4460c4fea60d5a915b73861d00876a8f1fa50936100ae8f72ad4412102ae912ff4cf65d91f8174fc8620ea4c627fb9ae282a915ff2fa3dd31044971177ffffffff0200000000000000004c006a4953454e534f52415f50524f4f4601012a0104f81c1b83c30000000000000001000000006878cec70001727e4a61a0971fdebe760d73e8a9de80e507062482806701eced6c7ee3df20bd87020000000000001976a9146c6ec50d57d4ac54ff23ff6482fb6695070c2d7a88ac000000000100", + }, + "beef v2 from the above BeefSet [mined]": { + hex: BEEFSet, + }, + "beef v1 from the above BRC62Hex [mined]": { + hex: BRC62Hex, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // Decode BEEF hex + beefData, err := hex.DecodeString(test.hex) + require.NoError(t, err) + + // Parse BEEF + beef, err := NewBeefFromBytes(beefData) + require.NoError(t, err) + + // Count success/failure + successCount := 0 + failureCount := 0 + + // Run validation multiple times + for i := 0; i < iterations; i++ { + if beef.IsValid(true) { + successCount++ + } else { + failureCount++ + } + } + + // Debug: Check what's in the BEEF + if failureCount > 0 || successCount == 0 { + t.Logf("BEEF has %d transactions and %d BUMPs", len(beef.Transactions), len(beef.BUMPs)) + for txid, tx := range beef.Transactions { + if tx.Transaction != nil { + t.Logf("Transaction %s has %d inputs, MerklePath: %v", txid, len(tx.Transaction.Inputs), tx.Transaction.MerklePath != nil) + for i, input := range tx.Transaction.Inputs { + t.Logf(" Input %d: %s", i, input.SourceTXID.String()) + } + } + } + } + + // Log results + t.Logf("Success count: %d", successCount) + t.Logf("Failure count: %d", failureCount) + + // Check for consistency - all iterations should have the same result + if successCount > 0 && failureCount > 0 { + t.Errorf("Inconsistent validation results: %d successes, %d failures", successCount, failureCount) + } + }) + } +} From 53579045a07a04a59042ef71cbebdf19a9e441e3 Mon Sep 17 00:00:00 2001 From: David Case Date: Thu, 17 Jul 2025 14:13:14 -0400 Subject: [PATCH 2/9] Update CHANGELOG.md for version 1.2.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 917c33e7..6498a239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format ## Table of Contents +- [1.2.6 - 2025-07-17](#126---2025-07-17) - [1.2.5 - 2025-07-16](#125---2025-07-16) - [1.2.4 - 2025-06-30](#124---2025-06-30) - [1.2.3 - 2025-06-30](#123---2025-06-30) @@ -40,6 +41,11 @@ All notable changes to this project will be documented in this file. The format - [1.1.0 - 2024-08-19](#110---2024-08-19) - [1.0.0 - 2024-06-06](#100---2024-06-06) +## [1.2.6] - 2025-07-17 + +### Fixed +- Fixed BEEF validation stability issue where `IsValid` returned inconsistent results (#211) + ## [1.2.5] - 2025-07-16 ### Changed From f5ea40149211915fe71cd0e165d601be781b6a12 Mon Sep 17 00:00:00 2001 From: David Case Date: Thu, 17 Jul 2025 14:18:39 -0400 Subject: [PATCH 3/9] Fix README installation instructions Use 'go get' instead of 'go install' for adding the SDK as a dependency. The go-sdk is a library package, not an executable. Fixes #202 --- CHANGELOG.md | 1 + README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6498a239..2efd79fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ All notable changes to this project will be documented in this file. The format ### Fixed - Fixed BEEF validation stability issue where `IsValid` returned inconsistent results (#211) +- Fixed README installation instructions to use `go get` instead of `go install` (#202) ## [1.2.5] - 2025-07-16 diff --git a/README.md b/README.md index 48b7bd6e..0ec5ae3e 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ The BSV Blockchain Libraries Project aims to structure and maintain a middleware ### Installation -To install the SDK, run: +To add the SDK to your Go module: ```bash -go install github.com/bsv-blockchain/go-sdk +go get github.com/bsv-blockchain/go-sdk ``` ### Basic Usage From bec9eef80aef0b2793445b7c1ff998a21aa07bf9 Mon Sep 17 00:00:00 2001 From: David Case Date: Thu, 17 Jul 2025 18:16:07 -0400 Subject: [PATCH 4/9] Refactor BEEF validation logic - Fix validation to check BUMPs properly - Rename SortTxs to ValidateTransactions - Verify bump indices for RawTxAndBumpIndex - Fix test assumptions --- CHANGELOG.md | 6 + transaction/beef.go | 295 +++++++++++++++++++++++------------ transaction/beef_test.go | 17 +- transaction/testdata/bump.go | 3 + 4 files changed, 211 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2efd79fc..99ff0fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,8 +45,14 @@ All notable changes to this project will be documented in this file. The format ### Fixed - Fixed BEEF validation stability issue where `IsValid` returned inconsistent results (#211) +- Fixed BEEF parsing panic when encountering transactions without merkle paths (#96) +- Fixed validation logic to properly check if transactions appear in BUMPs - Fixed README installation instructions to use `go get` instead of `go install` (#202) +### Changed +- Renamed `SortTxs()` method to `ValidateTransactions()` for clarity +- Improved BEEF validation to handle transactions without source transactions gracefully + ## [1.2.5] - 2025-07-16 ### Changed diff --git a/transaction/beef.go b/transaction/beef.go index c5d3e6de..77e66b31 100644 --- a/transaction/beef.go +++ b/transaction/beef.go @@ -368,13 +368,10 @@ func readAllTransactions(reader *bytes.Reader, BUMPs []*MerklePath) (map[string] sourceTxid := input.SourceTXID.String() if sourceObj, ok := transactions[sourceTxid]; ok { input.SourceTransaction = sourceObj - } else if tx.MerklePath == nil { - panic(fmt.Sprintf( - "There is no Merkle Path or Source Transaction for outpoint: %s, %d", - sourceTxid, - input.SourceTxOutIndex, - )) } + // Note: Unlike the previous implementation, we don't panic here if the source + // transaction is missing. This matches the TypeScript SDK behavior where + // source transaction assignment is deferred until needed. } transactions[txid.String()] = tx } @@ -803,104 +800,214 @@ func (b *Beef) Verify(ctx context.Context, chainTracker chaintracker.ChainTracke return true, nil } -type SortResult struct { - MissingInputs []string - NotValid []string - Valid []string - WithMissingInputs []string - TxidOnly []string +// ValidationResult contains the results of transaction validation +type ValidationResult struct { + Valid []string // Transactions that are fully validated + NotValid []string // Transactions that cannot be validated + TxidOnly []string // Transactions represented only by txid + WithMissingInputs []string // Transactions with inputs not in BEEF + MissingInputs []string // Input txids that are missing } -// SortTxs sorts the transactions in the BEEF by dependency order. -func (b *Beef) SortTxs() SortResult { +// ValidateTransactions validates the transactions in this BEEF and sorts them by dependency order. +// It returns a ValidationResult containing the validation status of each transaction. +// +// Validation rules: +// - Transactions with merkle paths are automatically valid +// - Transactions without merkle paths must have all inputs traceable to transactions with merkle paths +// - For DataFormat == RawTx or TxIDOnly, checks if the txid appears in BUMPs (has proof) +// - For DataFormat == RawTxAndBumpIndex, verifies the bump index is accurate +func (b *Beef) ValidateTransactions() *ValidationResult { + // Build a map of txids that appear in BUMPs (have proof) + txidsInBumps := make(map[string]bool) + for _, bump := range b.BUMPs { + if len(bump.Path) > 0 { + // Check level 0 (leaf level) for transaction hashes + for _, elem := range bump.Path[0] { + if elem.Hash != nil && elem.Txid != nil && *elem.Txid { + txidsInBumps[elem.Hash.String()] = true + } + } + } + } - res := SortResult{} + result := &ValidationResult{ + MissingInputs: []string{}, + NotValid: []string{}, + Valid: []string{}, + WithMissingInputs: []string{}, + TxidOnly: []string{}, + } + + // Maps for tracking + validTxids := make(map[string]bool) + txByID := make(map[string]*BeefTx) + missingInputs := make(map[string]bool) - // Collect all transactions into a slice for sorting and keep track of which txid is valid - allTxs := make([]*BeefTx, 0, len(b.Transactions)) - validTxids := map[string]bool{} - missing := map[string]bool{} + // Lists for processing + var hasProof []*BeefTx + var txidOnly []*BeefTx + var needsValidation []*BeefTx + var withMissingInputs []*BeefTx + // First pass: categorize transactions for txid, beefTx := range b.Transactions { - allTxs = append(allTxs, beefTx) - // Mark transactions with proof or no inputs as valid - if beefTx.Transaction != nil && beefTx.Transaction.MerklePath != nil { - validTxids[txid] = true - } else if beefTx.DataFormat == TxIDOnly && beefTx.KnownTxID != nil { - res.TxidOnly = append(res.TxidOnly, txid) - validTxids[txid] = true - } - } + txByID[txid] = beefTx - // Separate transactions that have at least one missing input - queue := make([]*BeefTx, 0) - for _, beefTx := range allTxs { - if beefTx.Transaction != nil { - hasMissing := false - for _, in := range beefTx.Transaction.Inputs { - if !validTxids[in.SourceTXID.String()] && b.findTxid(in.SourceTXID.String()) == nil { - missing[in.SourceTXID.String()] = true - hasMissing = true - } + switch beefTx.DataFormat { + case TxIDOnly: + // TxIDOnly transactions are valid if they appear in BUMPs + if txidsInBumps[txid] { + validTxids[txid] = true + txidOnly = append(txidOnly, beefTx) + } else { + // TxIDOnly without proof - add to txidOnly list but not valid + txidOnly = append(txidOnly, beefTx) } - if hasMissing { - res.WithMissingInputs = append(res.WithMissingInputs, beefTx.Transaction.TxID().String()) + case RawTxAndBumpIndex: + // Verify the bump index is accurate + if beefTx.BumpIndex >= 0 && beefTx.BumpIndex < len(b.BUMPs) { + bump := b.BUMPs[beefTx.BumpIndex] + // Check if this transaction appears in the specified bump + foundInBump := false + for _, elem := range bump.Path[0] { + if elem.Hash != nil && elem.Hash.String() == txid { + foundInBump = true + break + } + } + if foundInBump { + validTxids[txid] = true + hasProof = append(hasProof, beefTx) + } else { + // Invalid bump index - treat as needing validation + needsValidation = append(needsValidation, beefTx) + } } else { - queue = append(queue, beefTx) + // Invalid bump index - treat as needing validation + needsValidation = append(needsValidation, beefTx) + } + case RawTx: + // RawTx is valid if it appears in a BUMP + if txidsInBumps[txid] { + validTxids[txid] = true + hasProof = append(hasProof, beefTx) + } else if beefTx.Transaction != nil { + // Check if all inputs are available + hasMissing := false + for _, input := range beefTx.Transaction.Inputs { + if _, exists := b.Transactions[input.SourceTXID.String()]; !exists { + missingInputs[input.SourceTXID.String()] = true + hasMissing = true + } + } + if hasMissing { + withMissingInputs = append(withMissingInputs, beefTx) + } else { + needsValidation = append(needsValidation, beefTx) + } } } } - // Try to validate any transactions whose inputs are now known - oldLen := -1 - for oldLen != len(queue) { - oldLen = len(queue) - newQueue := make([]*BeefTx, 0, len(queue)) - for _, beefTx := range queue { + // Iteratively validate transactions that depend on other transactions + for len(needsValidation) > 0 { + progress := false + var stillNeedsValidation []*BeefTx + + for _, beefTx := range needsValidation { + // Check if all inputs are valid + allInputsValid := true if beefTx.Transaction != nil { - allInputsValid := true - for _, in := range beefTx.Transaction.Inputs { - if !validTxids[in.SourceTXID.String()] { + for _, input := range beefTx.Transaction.Inputs { + if !validTxids[input.SourceTXID.String()] { allInputsValid = false break } } - if allInputsValid { - validTxids[beefTx.Transaction.TxID().String()] = true - res.Valid = append(res.Valid, beefTx.Transaction.TxID().String()) - } else { - newQueue = append(newQueue, beefTx) + } + + if allInputsValid { + txid := beefTx.Transaction.TxID().String() + validTxids[txid] = true + hasProof = append(hasProof, beefTx) + progress = true + } else { + stillNeedsValidation = append(stillNeedsValidation, beefTx) + } + } + + needsValidation = stillNeedsValidation + if !progress { + // No progress made - remaining transactions are not valid + for _, beefTx := range needsValidation { + if beefTx.Transaction != nil { + result.NotValid = append(result.NotValid, beefTx.Transaction.TxID().String()) } } + break + } + } + + // Populate result lists + // Add transactions with missing inputs + for _, beefTx := range withMissingInputs { + if beefTx.Transaction != nil { + txid := beefTx.Transaction.TxID().String() + result.WithMissingInputs = append(result.WithMissingInputs, txid) + } + } + + // Add txid-only transactions + for _, beefTx := range txidOnly { + var txid string + if beefTx.KnownTxID != nil { + txid = beefTx.KnownTxID.String() + } else if beefTx.Transaction != nil { + txid = beefTx.Transaction.TxID().String() + } else { + continue + } + result.TxidOnly = append(result.TxidOnly, txid) + if validTxids[txid] { + result.Valid = append(result.Valid, txid) } - queue = newQueue } - // Now, whatever is left in queue is not valid - for _, beefTx := range queue { + // Add valid transactions with proofs (in dependency order) + for _, beefTx := range hasProof { if beefTx.Transaction != nil { - res.NotValid = append(res.NotValid, beefTx.Transaction.TxID().String()) + txid := beefTx.Transaction.TxID().String() + result.Valid = append(result.Valid, txid) } } - for k := range missing { - res.MissingInputs = append(res.MissingInputs, k) + // Populate missing inputs list + for txid := range missingInputs { + result.MissingInputs = append(result.MissingInputs, txid) } - return res + + return result } func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { r := verifyResult{valid: false, roots: map[uint32]string{}} - b.SortTxs() // Assume this sorts transactions in dependency order - + + // Validate and sort transactions + vr := b.ValidateTransactions() + + // Check if validation passed + if len(vr.MissingInputs) > 0 || + len(vr.NotValid) > 0 || + (len(vr.TxidOnly) > 0 && !allowTxidOnly) || + len(vr.WithMissingInputs) > 0 { + return r + } + + // Build valid txids set txids := make(map[string]bool) - for _, tx := range b.Transactions { - if tx.DataFormat == TxIDOnly { - if !allowTxidOnly { - return r - } - txids[tx.KnownTxID.String()] = true - } + for _, txid := range vr.Valid { + txids[txid] = true } confirmComputedRoot := func(mp *MerklePath, txid string) bool { @@ -919,51 +1026,35 @@ func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { return true } + // Verify all BUMPs have consistent roots for _, mp := range b.BUMPs { for _, n := range mp.Path[0] { - if n.Txid != nil && n.Hash != nil { + if n.Txid != nil && *n.Txid && n.Hash != nil { if !confirmComputedRoot(mp, n.Hash.String()) { return r } - txids[n.Hash.String()] = true } } } - // Single pass: add transactions with merkle paths to valid set and collect those needing validation - remaining := make(map[string]*BeefTx) + // Verify all transactions with BumpIndex have matching txid in the BUMP for txid, beefTx := range b.Transactions { - if beefTx.Transaction != nil && beefTx.Transaction.MerklePath != nil { - txids[txid] = true - } else if beefTx.DataFormat != TxIDOnly && beefTx.Transaction != nil { - remaining[txid] = beefTx - } - } - - // Keep processing until we've validated all transactions or can't make progress - for len(remaining) > 0 { - progress := false - for txid, beefTx := range remaining { - // Check if all inputs are valid - allInputsValid := true - for _, in := range beefTx.Transaction.Inputs { - if !txids[in.SourceTXID.String()] { - allInputsValid = false + if beefTx.DataFormat == RawTxAndBumpIndex { + if beefTx.BumpIndex < 0 || beefTx.BumpIndex >= len(b.BUMPs) { + return r + } + bump := b.BUMPs[beefTx.BumpIndex] + found := false + for _, leaf := range bump.Path[0] { + if leaf.Hash != nil && leaf.Hash.String() == txid { + found = true break } } - - if allInputsValid { - txids[txid] = true - delete(remaining, txid) - progress = true + if !found { + return r } } - - // If we didn't make progress, the remaining transactions have missing inputs - if !progress { - return r - } } r.valid = true @@ -1127,7 +1218,7 @@ func (b *Beef) trimUnreferencedBumps() { } func (b *Beef) GetValidTxids() []string { - r := b.SortTxs() + r := b.ValidateTransactions() return r.Valid } diff --git a/transaction/beef_test.go b/transaction/beef_test.go index 2e240425..40b78032 100644 --- a/transaction/beef_test.go +++ b/transaction/beef_test.go @@ -195,7 +195,7 @@ func TestBeefSortTxs(t *testing.T) { } // Test SortTxs - result := beef.SortTxs() + result := beef.ValidateTransactions() require.NotNil(t, result) // Log the results @@ -405,7 +405,7 @@ func TestBeefGetValidTxids(t *testing.T) { } // Get sorted transactions to see what's valid - sorted := beef.SortTxs() + sorted := beef.ValidateTransactions() t.Log("\nSorted transaction results:") t.Logf(" Valid: %v", sorted.Valid) t.Logf(" TxidOnly: %v", sorted.TxidOnly) @@ -417,8 +417,8 @@ func TestBeefGetValidTxids(t *testing.T) { validTxids := beef.GetValidTxids() t.Logf("\nGetValidTxids result: %v", validTxids) - // Verify results match - require.Equal(t, sorted.Valid, validTxids, "GetValidTxids should return same txids as SortTxs.Valid") + // Verify results match (order doesn't matter) + require.ElementsMatch(t, sorted.Valid, validTxids, "GetValidTxids should return same txids as ValidateTransactions.Valid") // If we have any valid transactions, verify they exist and have valid inputs if len(validTxids) > 0 { @@ -427,10 +427,11 @@ func TestBeefGetValidTxids(t *testing.T) { require.NotNil(t, tx, "Valid txid should exist in transactions map") // If it has a transaction, verify it has no missing inputs - if tx.Transaction != nil { + // (unless it has a merkle path, in which case it's already proven) + if tx.Transaction != nil && tx.Transaction.MerklePath == nil { for _, input := range tx.Transaction.Inputs { sourceTx := beef.findTxid(input.SourceTXID.String()) - require.NotNil(t, sourceTx, "Input transaction should exist for valid transaction") + require.NotNil(t, sourceTx, "Input transaction should exist for valid transaction without merkle path") } } } @@ -465,7 +466,7 @@ func TestBeefFindTransactionForSigning(t *testing.T) { } // Get sorted transactions to see what's valid - sorted := beef.SortTxs() + sorted := beef.ValidateTransactions() t.Log("\nSorted transaction results:") t.Logf(" Valid: %v", sorted.Valid) t.Logf(" TxidOnly: %v", sorted.TxidOnly) @@ -736,7 +737,7 @@ func TestBeefEdgeCases(t *testing.T) { require.NotNil(t, tx.KnownTxID, "TxIDOnly transaction should have KnownTxID") // Test that TxIDOnly transactions are properly categorized - sorted := beef.SortTxs() + sorted := beef.ValidateTransactions() require.NotContains(t, sorted.Valid, txid, "TxIDOnly transaction should not be considered valid") require.Contains(t, sorted.TxidOnly, txid, "TxIDOnly transaction should be in TxidOnly list") diff --git a/transaction/testdata/bump.go b/transaction/testdata/bump.go index dbf7409d..0c2355ed 100644 --- a/transaction/testdata/bump.go +++ b/transaction/testdata/bump.go @@ -10,6 +10,9 @@ var ValidBumps = []BumpTest{ {Bump: "feb39d0c000c02fd340700ed4cb1fdd81916dabb69b63bcd378559cf40916205cd004e7f5381cc2b1ea6acfd350702957998e38434782b1c40c63a4aca0ffaf4d5d9bc3385f0e9e396f4dd3238f0df01fd9b030012f77e65627c341a3aaea3a0ed645c0082ef53995f446ab9901a27e4622fd1cc01fdcc010074026299a4ba40fbcf33cc0c64b384f0bb2fb17c61125609a666b546539c221c01e700730f99f8cf10fccd30730474449172c5f97cde6a6cf65163359e778463e9f2b9017200a202c78dee487cf96e1a6a04d51faec4debfad09eea28cc624483f2d6fa53d54013800b51ecabaa590b6bd1805baf4f19fc0eae0dedb533302603579d124059b374b1e011d00a0f36640f32a43d790bb4c3e7877011aa8ae25e433b2b83c952a16f8452b6b79010f005d68efab62c6c457ce0bb526194cc16b27f93f8a4899f6d59ffffdddc06e345c01060099f66a0ef693d151bbe9aeb10392ac5a7712243406f9e821219fd13d1865f569010200201fa17c98478675a96703ded42629a3c7bf32b45d0bff25f8be6849d02889ae010000367765c2d68e0c926d81ecdf9e3c86991ccf5a52e97c49ad5cf584c8ab030427010100237b58d3217709b6ebc3bdc093413ba788739f052a0b5b3a413e65444b146bc1"}, } +// This hex is from issue #96 - it was failing with "no leaves at height: 1" error +var Issue96BeefHex = "0100beef05fe3763190001020000e29fffbb85699957a9ab28299f182218b89873f9e3ca2ed81f2623502fcde7a10102c60ebb010343021cf98872306f484b906acc038b1deecfdf19673386eb7feb6bfe3963190005020000a04cd063b9dcfc44d10749acbc997a341b70d6004e84daa262807da7aff9d8920102045660c3170ef44027f44bdd1d7e2a57a70dd29040873655e122f541979e570e0101009a81b73ef18aed0bab8b151d1715792caf8dd3e7be4b1956e54a436a33692807010100b7050f4dd84980343211994eed88df39f7906a0b7a80c658b15ebb5c8891a1d7010100e694b36062b7586daf25c700cbddd9209c99c537e99c7123043d0d8ee4781745010100bd7e2599fd6d384c25bc4f619b0c75fe5d0c91a4b2356e3b2d5ab566f433fee6fe3c63190004040000968ef88320100a445d7f36793f2e6513a64a5370969b6a5837be156f87b2881e010273fa7fc6d88c8b61c5973abac80653255bfe57c83fb337fa7aeaacd8b5dc526402025b0b1a18502aa4cd896102b35362b6861728b1f14f4ed5c6197e83d17a16927a030223d4af97499600ee164f846a28b20d8a7cf5dbae2ef67012514d560761328abc000101004456c093bdbfe719cc33ce106c11e112190127d8664b10e21c0be1ff4bc5edf1010100efa02a414be05a36f67d7d9db5f0363e34d9e5c3dd480396cb92f5a5c54cec24fe43631900010200007c577df26a7ab429e0cd394aa09b6889a8f4aca6e31df5bc15f28f6103c5b5880102d9f4c0796c1db1fc1efe4d5ccf5aecba5786c1fcac98f14d3c1972357fdb3c0afe6e621900010200008cdbd6fe227cfafa6be4500cae6140c84b2ead61318063e92bdb6974f869cf33010259617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc5070140e010000000cc60ebb010343021cf98872306f484b906acc038b1deecfdf19673386eb7feb6b000000006a47304402206ab6babb9e60c3a681e09611a2af513c63a94fff5752a7b9b17b3910e6652d1a02207994b669a72449456a0710f6a514ae0097198962a19fa8d9e58eeed66adc81ea4121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff045660c3170ef44027f44bdd1d7e2a57a70dd29040873655e122f541979e570e000000006b483045022100a9da20a540ae2309fccb48d0122c24ec1dab6af750a8e63268b861aced14c0fd02207d0518f38a4a1b9fb9a64e97d1917e1b5005115fa8b79db64e8cb992d43b79964121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff73fa7fc6d88c8b61c5973abac80653255bfe57c83fb337fa7aeaacd8b5dc5264010000006a473044022012e0ba77c100ee3e54e300fd8d8e7ad7db92f669be1a495fa6acde81b0e799ef02204c0886bc6040bb728d7add8c137bd2e3473cfd21d0d041a370a5bc20f67beeb04121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff5b0b1a18502aa4cd896102b35362b6861728b1f14f4ed5c6197e83d17a16927a010000006a47304402204d62b7a3164c6010398ab3f34caf16cac24ac1767a73d2f86690deb60f24d828022052a2e6bb49bfdb690a8d1e104598ce28c188da5caf7c1231dbfbc0c5fbd73d3b4121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff23d4af97499600ee164f846a28b20d8a7cf5dbae2ef67012514d560761328abc010000006a473044022015c70bd3d3f4ed6193a151a9affd2fe698c5fce35d41bd4fa0a062de5d18531a02207b8537ccee79fe1077f94e47298c5e88a93cb7194505c97937f7237bd45fdca14121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffffd9f4c0796c1db1fc1efe4d5ccf5aecba5786c1fcac98f14d3c1972357fdb3c0a000000006a4730440220400cd17bc933955cedbdb8c05b9638bd1bc85c188d7384582adb266da484d146022021a86979b12373615b6979ed99715f8cb90df90a0afd978dd940f47d992d6ef24121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff61e19da2629900ba8be5716ef767ba7d60e0f86e96b246b3ff224536ecb606ca000000006b4830450221008dc5f8438885760206114a90daf261769f73f14b2c333b2683cebd931438e8bd02200d8f7ee950c62373dab96121ea4d7e6ea8b7b00ca5eb3d84590ff7425924066d4121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffffde750a577972bc7836871dcbef495cb71d77377e5c9ad1c0259245167fac53d0000000006b483045022100da367ba4ac7d3b92fa1c370f095a1833cfa4343c303cf7f08b370656dad66a2802204c222e0c0029bdc0d6ee16751ee25f1b15ed7afe6776bfe8df5a002401bedd1c4121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffffeb4a0cd282249bd4e0a3d8c70cb33e045105506caebff8fd6afa93c2834bfa60000000006b483045022100aa1e1a5c1ad4f30043af96892d4aea2b54a2cd861c2d35f3ca1038056019207102201882ca15c7058123aea54ac59ff8ae1f3506031d704f78283d1c3cf716d12f644121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff5c61ae618cdbd4d4368f0482d83318c145ee072dac5c3912db9dfcefd1a9aedf010000006a473044022048c626b5b8302ad617d78e07a121009117e682816010a284752262a7bf7259cb022035bc754bbea7a3146b8009bd618646b2c06c82d35873607cebabbb14a222a6194121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffffae2bdb78cd7f2b378c009c92a53b3bd43c1dbc853e908ca688d15ea379f7b019000000006a473044022043504e1317ee042e3dcf47b3f67b4378f30a3a3133d32f26253c6e052df26a9e02205497d0d7a4a431c7c7ab5db96452c03b0dbb4c0cd39e0f32b8374c0988e5dcf34121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffffb73c101a69f724f8583da8006d8acf313a35470e61f098bb3c70330b5fd63139000000006a47304402205710cfde3589f5c6e97138fd209631043711e06b2677f65408c26e5d705dc6910220021fcbaf71763e102d7808cfe398edd9ca0b7367ddea03b62f6108d48c8035af4121025a270010d0392c61a0ee274d88287b5d4db563c6afa4574615181e6769ab47d3ffffffff010a000000000000001976a914cd9cb8276894790648a7dd68e917c6029251de6e88ac000000000001000000015de78940b72e2e2d3a0e0a795fde144d48bbf52b1ad0e87f8d42736234ed574b000000006a47304402202e1ee6b75f96ca96e127d8d09d7ad8c4d3c93548ae90d2b814acc95cbc115a280220293619baacefac2667dd19c8119a8b99c88a70004a322c1310f539b94c5e4a75412103dbc21ebd13da57220deff50a0465799151ce682a5dc2bd13dcb6809f413c4108ffffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac01000000000000001976a914c753bcccbfd437301b382587951516250edc2d7788ac000000000100010000000159617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc507014130000006b4830450221009399820c643e5f2699b07ffa3289b22c6124e1317fd1ec41fb2431047dcf552f02201b7ee3ee3422fe1c33d8869a34a28fadddb9b790b3c5b332b7fafaaa8d4b413c4121029b15053bc379e2378cd6a84fb40b761b4e400faa4efd09280443731d4b3f8a8cffffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac03000000000000001976a9144ed83e2b3aae481f7fa48321024eb3d8e1f7417888ac0000000001010100000001045660c3170ef44027f44bdd1d7e2a57a70dd29040873655e122f541979e570e010000006b483045022100d3f3502c419a585f90c9584212fd9db2eb90eb388ffbc8f3b25adc36af5e6c620220687a84086fc458d9c97e38fa0b5c043ed5411939a15623ad208566178da9d931412102e3bee971d5b655dfd5a7e18eb7947791421db1968bae5a16f81369a4e0fad674ffffffff0201000000000000001976a914f8cfe0e2a34b16c5c463ae9f19ecc8a04913e33588ac01000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac000000000102010000000159617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc5070140a0000006b483045022100ec7c726828d672a724d2e35583fa6bad1cf50a85e90fdd8af8da0a532ef83ab502200db3c7c7fe7f557501ad8523620dc76316faa633cb0289d96408d2199eed93b6412102e74cc80c381ed86d4b2f8fe0b6681f21bb97ef1e99f13d31d99832b83b7169f3ffffffff0203000000000000001976a9149bbd5a31e27a6ebcefbde5957b0d34e01726bb2788ac01000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac00000000010201000000015b0b1a18502aa4cd896102b35362b6861728b1f14f4ed5c6197e83d17a16927a000000006b483045022100dfd895883db0a4bc23b6aca041708dc48830a68e456f3442c3779c3ef858a8e40220354d339d4db225468161674774230104371bde3322c1fe45e8a3b8817a594f2d4121023a2dfb6c2fa94cd96f05cda46b04654663af00c198cd8d47085d2b1c8b95b177ffffffff0201000000000000001976a914af863962136cc2e96409345aebdc12ef0eaef26188ac01000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac000000000102010000000159617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc5070141b0000006b483045022100a8988ca7dfc9db558ecfa282089e047d65cd1e48331681bdcdc0a9b02dbf738f02202609052607b61172ed80abc530fbe100eae3ce8ea34fd1216b1189a6076ca9c1412103db623562410ab6999e4dd32c0ce178dff7ea607f41d621ba33506e1d4cf7d9ccffffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac03000000000000001976a914f1ba3a1ab6d9f383657d59ebb2137465f2cb7d8a88ac0000000001030100000001ee353cb1f4cb2b19bf7d2328f3c0a5fa2bd1aa94d0795934dd1d63756583b0ac000000006b4830450221009e779c3f04bb056da180b50ad3972b6fd552c52bd6337f572f08ca656625a4530220648e1fc5872d9bc3846f78edd79b4dc406dfecbab359e46b53c7d478747141684121022280e60c665907fb88a97f5e74dd4e1db0eba60a4dad18158ea8eb9495e242daffffffff2005000000000000001976a914e073bf1bf7b6160f7f68403a374717fdc4cce86d88ac4a000000000000001976a9145bbe83249067ec745d8e270c121c651f56ec2cf188ac42030000000000001976a914983a29113517769da9eace0ec25b760ad09c5f5988ac05000000000000001976a91402c75f3ec99db76e6cd2658a7c83c8bf21a0d14b88ac05000000000000001976a914de454fbce4e1be341d11cd5545664771f72d184088ac25000000000000001976a914b5c2a6831b5cdbc8985517f5d912064449ad118088ac3d010000000000001976a914e4566d91c063f99f7418f12d670b5da320160c8188ac05000000000000001976a914616dc74c3945895eafdac8da9174f95b4d91d75b88ac03010000000000001976a91476ae1c3f2c01a9a5f2aa7696cc9b89ea55b0d01688ac0d000000000000001976a9146d805d21013cc20351e5cd55ecaa7dcee59f2fde88ac05000000000000001976a914613408f68bdde1ff3f77a96abd4956a472766acf88ac21000000000000001976a914d7f0bcfa5b5cad93574a960627db60af8dac277c88ac05000000000000001976a9147e088fe21413442569fa59b6a5bf7e8fe6db71e388ac05000000000000001976a914e5ff336cbffc4ffef544ff4d131de68bf9b0aa1488ac05000000000000001976a9142145b563cd2766ba08d33ca0ecb5a6679268dfb788ac05000000000000001976a914da990487dff4910c8891cebcd00a569011f4f05188ac05000000000000001976a914100daf8fa5add521fb87b46ff51657728626c5fe88ac07000000000000001976a914de900668ddecd73f498c8560b8770231bef9616b88ac05000000000000001976a914dd08ccc14ebdcc534bb2320e7d3dba624a7ecd5388ac05000000000000001976a9141c5b5915e67a840fdcefd0db0ca1a1dfaaf4f3cf88ac06000000000000001976a91441815c6352756937fe6b2b17586dd6b39560321188ac05000000000000001976a91471dca8f8631f145ad5721f8387a764ae0ef2599888ac1d000000000000001976a9147e2621372c66ca3c6e6fca8cdb7eac42405d37fe88ac05000000000000001976a914d745ec07218bde8d179d49e4a6feca7f744f57e388ac05000000000000001976a91499456246822c4270ae73d6c7d8e1af690d1d31dd88ac05000000000000001976a914176dc7f479059d28593021e63bcb66bae6c2cc5388ac05000000000000001976a914a48809ce3c763a0a34c24ab2646fdf10cffdbc6588ac05000000000000001976a914e314da9f12f6bd888da53faa616890a2dce9eeb488ac05000000000000001976a91418f9185bb0ca144d934bd4845b3968d545e7ec8188ac17010000000000001976a9149db831932668077f8eebf597976ec61fc6ab90d588ac09000000000000001976a91479b349601390a6216994d44ca618db71e6e811ef88ac06000000000000001976a91451d25ac626a4b7c281b4200a3dcf34caae43f0df88ac0000000001040100000001d9f4c0796c1db1fc1efe4d5ccf5aecba5786c1fcac98f14d3c1972357fdb3c0a010000006b4830450221009a95e614f69f2d300e541f84d3dd4c3b1247f2a0e33e2d4824c01b27c131e2e402206ca8b98f0d60a7599e40c22f7b12341e1c9deede5a878f5b8058daa5df08068741210204f5ac8fe5f95e8f45ce44c68c6ee1af5756e47ea8981f5a9cbd9c8871b43fbfffffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac01000000000000001976a914e3aab0e65b69d0206eacc9ba6a66a7d6418e48b488ac0000000000010000000159617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc507014070000006b483045022100907eec2cc7d8fd051ac6aac6c8a1ec3b1452b8d9b8c93c2742401ac298118e2002207f39d2a1ec9322842cba006c8aaf30307c88e87a7935780b68ba0bac4f0df255412103918d5589f8e8af4f9343359a6b349f27511f891e1667952b2267290d9a7834feffffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac03000000000000001976a91467093ae40721810ee5f56ec71991c0b2dab1b9d288ac00000000000100000001de750a577972bc7836871dcbef495cb71d77377e5c9ad1c0259245167fac53d0010000006a47304402206c94c911e2d95b04aeab483ec6ca1d0214f7b86d1970476ac1a168c8b79fbe3002206e230db9b20bb665244b1e7528216d2ad15a90e75a0c4a2d8900f5ca2e6eeac64121034ee62aff712b5da30e92ac3c185ff3eee23ab83ddad49303d8574205e5e17c7dffffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac01000000000000001976a91495be8aaed53cdb2d7a84343e332f2652025b88be88ac0000000000010000000159617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc5070141c0000006a47304402203c1f25c5e41b4a102297c70b35c85530add87f050f244524a491450f4f16ede3022047cd556152fa78696300019c3406af21d82d23e453372c09d82436cbf636cbe9412103d1d8d4c9ca4c805e37456a003d510cb5b528946f2c63cdfd89d5386edbed8091ffffffff0203000000000000001976a914516ed9658f484da42879d94165e78647fccb8aaf88ac01000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac000000000001000000015c61ae618cdbd4d4368f0482d83318c145ee072dac5c3912db9dfcefd1a9aedf000000006b483045022100b30b58e738ca4e156d34563435d43ba3cd4f0b24ceccff0d9db1aa70786ffd75022058986fb08f83ada19e06216fb8d77796586f0e67271e1fe4889eecbe7baa33c7412102342ea8090d6bc310305d33ae6271e195a8ae0dde83f6383cc5aecf633c1acc0effffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac01000000000000001976a9141a5ba593f23bc897332033324ead93470f4c3ca088ac0000000000010000000159617a9d17562f7c9765e5dfa6a9a393aa2809ca6166a3d7a31c09efcc507014030000006b483045022100d1f4fe5671062bb423f53773fd4a08d11f3ee717119931058b2642aedd46dcd702207ac294b4892dcc4983aa8140f0486884e55a519bedd53e667bb8d04e107961ee41210304863a85badc27c818cacaca9e5582aa19738e3a60d5bf9a6354286c5858ed2affffffff0201000000000000001976a91423f2562a8092ed24eddc77c74387b44c561692a188ac03000000000000001976a91472b7782cd6e8e9d261eec14074a697b86050010e88ac0000000000" + var InvalidBumps = []BumpTest{ {Error: "Malformed BUMP", Bump: "feb39d0c000c01fd9b030012f77e65627c341a3aaea3a0ed645c0082ef53995f446ab9901a27e4622fd1cc01fdcc010074026299a4ba40fbcf33cc0c64b384f0bb2fb17c61125609a666b546539c221c01e700730f99f8cf10fccd30730474449172c5f97cde6a6cf65163359e778463e9f2b9017200a202c78dee487cf96e1a6a04d51faec4debfad09eea28cc624483f2d6fa53d54013800b51ecabaa590b6bd1805baf4f19fc0eae0dedb533302603579d124059b374b1e011d00a0f36640f32a43d790bb4c3e7877011aa8ae25e433b2b83c952a16f8452b6b79010f005d68efab62c6c457ce0bb526194cc16b27f93f8a4899f6d59ffffdddc06e345c01060099f66a0ef693d151bbe9aeb10392ac5a7712243406f9e821219fd13d1865f569010200201fa17c98478675a96703ded42629a3c7bf32b45d0bff25f8be6849d02889ae010000367765c2d68e0c926d81ecdf9e3c86991ccf5a52e97c49ad5cf584c8ab030427010100237b58d3217709b6ebc3bdc093413ba788739f052a0b5b3a413e65444b146bc1"}, // FIXME: failing tests From 867f5c0cd8dfcbeddf069268a3e0d3f51e5774da Mon Sep 17 00:00:00 2001 From: David Case Date: Fri, 18 Jul 2025 01:14:07 -0400 Subject: [PATCH 5/9] Refactor BEEF to use chainhash.Hash instead of strings - Use chainhash.Hash directly as map keys for better performance - Add *ByHash method variants to avoid unnecessary conversions - Update collectAncestors to return hash slice - Fix all tests to work with new hash-based implementation --- CHANGELOG.md | 3 + transaction/beef.go | 240 ++++++++++++++++++++----------------- transaction/beef_test.go | 95 ++++++++------- transaction/transaction.go | 2 +- 4 files changed, 185 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ff0fa9..d1fd36c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,9 @@ All notable changes to this project will be documented in this file. The format ### Changed - Renamed `SortTxs()` method to `ValidateTransactions()` for clarity - Improved BEEF validation to handle transactions without source transactions gracefully +- Refactored BEEF implementation to use `chainhash.Hash` directly as map keys instead of string conversions for improved performance +- Added `*ByHash` versions of BEEF methods (`findTxidByHash`, `FindBumpByHash`, etc.) to avoid unnecessary hash/string conversions +- Updated `collectAncestors` to return `[]chainhash.Hash` instead of `[]string` ## [1.2.5] - 2025-07-16 diff --git a/transaction/beef.go b/transaction/beef.go index 77e66b31..09717903 100644 --- a/transaction/beef.go +++ b/transaction/beef.go @@ -19,14 +19,14 @@ import ( type Beef struct { Version uint32 BUMPs []*MerklePath - Transactions map[string]*BeefTx + Transactions map[chainhash.Hash]*BeefTx } func NewBeef() *Beef { return &Beef{ Version: BEEF_V2, BUMPs: []*MerklePath{}, - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } } @@ -55,18 +55,18 @@ func newEmptyBeef(version uint32) *Beef { return &Beef{ Version: version, BUMPs: []*MerklePath{}, - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } } -func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[string]*BeefTx, error) { +func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[chainhash.Hash]*BeefTx, error) { var numberOfTransactions util.VarInt _, err := numberOfTransactions.ReadFrom(reader) if err != nil { return nil, err } - txs := make(map[string]*BeefTx, 0) + txs := make(map[chainhash.Hash]*BeefTx, 0) for i := 0; i < int(numberOfTransactions); i++ { formatByte, err := reader.ReadByte() if err != nil { @@ -87,7 +87,7 @@ func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[string]*BeefTx, if err != nil { return nil, err } - txs[txid.String()] = &beefTx + txs[txid] = &beefTx } else { bump := beefTx.DataFormat == RawTxAndBumpIndex // read the index of the bump @@ -110,13 +110,12 @@ func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[string]*BeefTx, } for _, input := range beefTx.Transaction.Inputs { - sourceTxid := input.SourceTXID.String() - if sourceObj, ok := txs[sourceTxid]; ok { + if sourceObj, ok := txs[*input.SourceTXID]; ok { input.SourceTransaction = sourceObj.Transaction } } - txs[beefTx.Transaction.TxID().String()] = &beefTx + txs[*beefTx.Transaction.TxID()] = &beefTx } } @@ -156,7 +155,7 @@ func NewBeefFromBytes(beef []byte) (*Beef, error) { } // run through the txs map and convert to BeefTx - beefTxs := make(map[string]*BeefTx, len(txs)) + beefTxs := make(map[chainhash.Hash]*BeefTx, len(txs)) for _, tx := range txs { if tx.MerklePath != nil { // find which bump index this tx is in @@ -168,13 +167,13 @@ func NewBeefFromBytes(beef []byte) (*Beef, error) { } } } - beefTxs[tx.TxID().String()] = &BeefTx{ + beefTxs[*tx.TxID()] = &BeefTx{ DataFormat: RawTxAndBumpIndex, Transaction: tx, BumpIndex: idx, } } else { - beefTxs[tx.TxID().String()] = &BeefTx{ + beefTxs[*tx.TxID()] = &BeefTx{ DataFormat: RawTx, Transaction: tx, } @@ -262,7 +261,7 @@ func NewBeefFromTransaction(t *Transaction) (*Beef, error) { beef := NewBeefV2() bumpMap := map[uint32]int{} txid := t.TxID() - txns := map[string]*Transaction{txid.String(): t} + txns := map[chainhash.Hash]*Transaction{*txid: t} ancestors, err := t.collectAncestors(txid, txns, false) if err != nil { return nil, err @@ -369,9 +368,6 @@ func readAllTransactions(reader *bytes.Reader, BUMPs []*MerklePath) (map[string] if sourceObj, ok := transactions[sourceTxid]; ok { input.SourceTransaction = sourceObj } - // Note: Unlike the previous implementation, we don't panic here if the source - // transaction is missing. This matches the TypeScript SDK behavior where - // source transaction assignment is deferred until needed. } transactions[txid.String()] = tx } @@ -395,7 +391,7 @@ func (t *Transaction) BEEF() ([]byte, error) { } bumps := []*MerklePath{} bumpMap := map[uint32]int{} - txns := map[string]*Transaction{t.TxID().String(): t} + txns := map[chainhash.Hash]*Transaction{*t.TxID(): t} ancestors, err := t.collectAncestors(nil, txns, false) if err != nil { return nil, err @@ -442,15 +438,14 @@ func (t *Transaction) BEEFHex() (string, error) { } } -func (t *Transaction) collectAncestors(txid *chainhash.Hash, txns map[string]*Transaction, allowPartial bool) ([]string, error) { +func (t *Transaction) collectAncestors(txid *chainhash.Hash, txns map[chainhash.Hash]*Transaction, allowPartial bool) ([]chainhash.Hash, error) { if txid == nil { txid = t.TxID() } - txidStr := txid.String() if t.MerklePath != nil { - return []string{txidStr}, nil + return []chainhash.Hash{*txid}, nil } - ancestors := make([]string, 0) + ancestors := make([]chainhash.Hash, 0) for _, input := range t.Inputs { if input.SourceTransaction == nil { if allowPartial { @@ -459,18 +454,17 @@ func (t *Transaction) collectAncestors(txid *chainhash.Hash, txns map[string]*Tr return nil, fmt.Errorf("missing previous transaction for %s", t.TxID()) } } - sourceTxid := input.SourceTXID.String() - txns[sourceTxid] = input.SourceTransaction + txns[*input.SourceTXID] = input.SourceTransaction if grands, err := input.SourceTransaction.collectAncestors(input.SourceTXID, txns, allowPartial); err != nil { return nil, err } else { ancestors = append(grands, ancestors...) } } - ancestors = append(ancestors, txidStr) + ancestors = append(ancestors, *txid) - found := make(map[string]struct{}) - results := make([]string, 0, len(ancestors)) + found := make(map[chainhash.Hash]struct{}) + results := make([]chainhash.Hash, 0, len(ancestors)) for _, ancestor := range ancestors { if _, ok := found[ancestor]; !ok { results = append(results, ancestor) @@ -481,14 +475,13 @@ func (t *Transaction) collectAncestors(txid *chainhash.Hash, txns map[string]*Tr return results, nil } -func (b *Beef) FindBump(txid string) *MerklePath { - idHash, _ := chainhash.NewHashFromHex(txid) - if idHash == nil { +func (b *Beef) FindBumpByHash(txid *chainhash.Hash) *MerklePath { + if txid == nil { return nil } for _, bump := range b.BUMPs { for _, leaf := range bump.Path[0] { - if leaf.Hash != nil && leaf.Hash.Equal(*idHash) { + if leaf.Hash != nil && leaf.Hash.Equal(*txid) { return bump } } @@ -496,14 +489,30 @@ func (b *Beef) FindBump(txid string) *MerklePath { return nil } -func (b *Beef) FindTransaction(txid string) *Transaction { +func (b *Beef) FindBump(txid string) *MerklePath { + idHash, err := chainhash.NewHashFromHex(txid) + if err != nil { + return nil + } + return b.FindBumpByHash(idHash) +} + +func (b *Beef) FindTransactionByHash(txid *chainhash.Hash) *Transaction { if beefTx := b.findTxid(txid); beefTx != nil { return beefTx.Transaction } return nil } -func (b *Beef) FindTransactionForSigning(txid string) *Transaction { +func (b *Beef) FindTransaction(txid string) *Transaction { + idHash, err := chainhash.NewHashFromHex(txid) + if err != nil { + return nil + } + return b.FindTransactionByHash(idHash) +} + +func (b *Beef) FindTransactionForSigningByHash(txid *chainhash.Hash) *Transaction { beefTx := b.findTxid(txid) if beefTx == nil { return nil @@ -511,7 +520,7 @@ func (b *Beef) FindTransactionForSigning(txid string) *Transaction { for _, input := range beefTx.Transaction.Inputs { if input.SourceTransaction == nil { - itx := b.findTxid(input.SourceTXID.String()) + itx := b.findTxid(input.SourceTXID) if itx != nil { input.SourceTransaction = itx.Transaction } @@ -521,7 +530,15 @@ func (b *Beef) FindTransactionForSigning(txid string) *Transaction { return beefTx.Transaction } -func (b *Beef) FindAtomicTransaction(txid string) *Transaction { +func (b *Beef) FindTransactionForSigning(txid string) *Transaction { + idHash, err := chainhash.NewHashFromHex(txid) + if err != nil { + return nil + } + return b.FindTransactionForSigningByHash(idHash) +} + +func (b *Beef) FindAtomicTransactionByHash(txid *chainhash.Hash) *Transaction { beefTx := b.findTxid(txid) if beefTx == nil { return nil @@ -529,19 +546,19 @@ func (b *Beef) FindAtomicTransaction(txid string) *Transaction { var addInputProof func(beef *Beef, tx *Transaction) addInputProof = func(beef *Beef, tx *Transaction) { - mp := beef.FindBump(tx.TxID().String()) + mp := beef.FindBumpByHash(tx.TxID()) if mp != nil { tx.MerklePath = mp } else { for _, input := range tx.Inputs { if input.SourceTransaction == nil { - itx := beef.findTxid(input.SourceTXID.String()) + itx := beef.findTxid(input.SourceTXID) if itx != nil { input.SourceTransaction = itx.Transaction } } if input.SourceTransaction != nil { - mp := beef.FindBump(input.SourceTransaction.TxID().String()) + mp := beef.FindBumpByHash(input.SourceTransaction.TxID()) if mp != nil { input.SourceTransaction.MerklePath = mp } else { @@ -557,6 +574,14 @@ func (b *Beef) FindAtomicTransaction(txid string) *Transaction { return beefTx.Transaction } +func (b *Beef) FindAtomicTransaction(txid string) *Transaction { + idHash, err := chainhash.NewHashFromHex(txid) + if err != nil { + return nil + } + return b.FindAtomicTransactionByHash(idHash) +} + func (b *Beef) MergeBump(bump *MerklePath) int { var bumpIndex *int // If this proof is identical to another one previously added, we use that first. @@ -607,31 +632,33 @@ func (b *Beef) MergeBump(bump *MerklePath) int { return *bumpIndex } -func (b *Beef) findTxid(txid string) *BeefTx { - if tx, ok := b.Transactions[txid]; ok { +func (b *Beef) findTxid(txid *chainhash.Hash) *BeefTx { + if txid == nil { + return nil + } + if tx, ok := b.Transactions[*txid]; ok { return tx } return nil } -func (b *Beef) MakeTxidOnly(txid string) *BeefTx { - tx, ok := b.Transactions[txid] +func (b *Beef) MakeTxidOnly(txid *chainhash.Hash) *BeefTx { + if txid == nil { + return nil + } + tx, ok := b.Transactions[*txid] if !ok { return nil } if tx.DataFormat == TxIDOnly { return tx } - if knownTxID, err := chainhash.NewHashFromHex(txid); err != nil { - return nil - } else { - tx = &BeefTx{ - DataFormat: TxIDOnly, - KnownTxID: knownTxID, - } - b.Transactions[txid] = tx - return tx + tx = &BeefTx{ + DataFormat: TxIDOnly, + KnownTxID: txid, } + b.Transactions[*txid] = tx + return tx } func (b *Beef) MergeRawTx(rawTx []byte, bumpIndex *int) (*BeefTx, error) { @@ -642,7 +669,7 @@ func (b *Beef) MergeRawTx(rawTx []byte, bumpIndex *int) (*BeefTx, error) { return nil, err } - txid := tx.TxID().String() + txid := tx.TxID() b.RemoveExistingTxid(txid) beefTx := &BeefTx{ @@ -658,15 +685,17 @@ func (b *Beef) MergeRawTx(rawTx []byte, bumpIndex *int) (*BeefTx, error) { beefTx.DataFormat = RawTxAndBumpIndex } - b.Transactions[txid] = beefTx + b.Transactions[*txid] = beefTx b.tryToValidateBumpIndex(beefTx) return beefTx, nil } // RemoveExistingTxid removes an existing transaction from the BEEF, given its TXID -func (b *Beef) RemoveExistingTxid(txid string) { - delete(b.Transactions, txid) +func (b *Beef) RemoveExistingTxid(txid *chainhash.Hash) { + if txid != nil { + delete(b.Transactions, *txid) + } } func (b *Beef) tryToValidateBumpIndex(tx *BeefTx) { @@ -683,7 +712,7 @@ func (b *Beef) tryToValidateBumpIndex(tx *BeefTx) { } func (b *Beef) MergeTransaction(tx *Transaction) (*BeefTx, error) { - txid := tx.TxID().String() + txid := tx.TxID() b.RemoveExistingTxid(txid) var bumpIndex *int @@ -701,7 +730,7 @@ func (b *Beef) MergeTransaction(tx *Transaction) (*BeefTx, error) { newTx.BumpIndex = *bumpIndex } - b.Transactions[txid] = newTx + b.Transactions[*txid] = newTx b.tryToValidateBumpIndex(newTx) if bumpIndex == nil { @@ -717,18 +746,17 @@ func (b *Beef) MergeTransaction(tx *Transaction) (*BeefTx, error) { return newTx, nil } -func (b *Beef) MergeTxidOnly(txid string) *BeefTx { +func (b *Beef) MergeTxidOnly(txid *chainhash.Hash) *BeefTx { + if txid == nil { + return nil + } tx := b.findTxid(txid) if tx == nil { - knownTxID, err := chainhash.NewHashFromHex(txid) - if err != nil { - return nil - } tx = &BeefTx{ DataFormat: TxIDOnly, - KnownTxID: knownTxID, + KnownTxID: txid, } - b.Transactions[txid] = tx + b.Transactions[*txid] = tx } return tx } @@ -737,9 +765,9 @@ func (b *Beef) MergeBeefTx(btx *BeefTx) (*BeefTx, error) { if btx == nil || btx.Transaction == nil { return nil, fmt.Errorf("nil transaction") } - beefTx := b.findTxid(btx.Transaction.TxID().String()) + beefTx := b.findTxid(btx.Transaction.TxID()) if btx.DataFormat == TxIDOnly && beefTx == nil { - beefTx = b.MergeTxidOnly(btx.KnownTxID.String()) + beefTx = b.MergeTxidOnly(btx.KnownTxID) } else if btx.Transaction != nil && (beefTx == nil || beefTx.DataFormat == TxIDOnly) { var err error beefTx, err = b.MergeTransaction(btx.Transaction) @@ -819,13 +847,13 @@ type ValidationResult struct { // - For DataFormat == RawTxAndBumpIndex, verifies the bump index is accurate func (b *Beef) ValidateTransactions() *ValidationResult { // Build a map of txids that appear in BUMPs (have proof) - txidsInBumps := make(map[string]bool) + txidsInBumps := make(map[chainhash.Hash]bool) for _, bump := range b.BUMPs { if len(bump.Path) > 0 { // Check level 0 (leaf level) for transaction hashes for _, elem := range bump.Path[0] { if elem.Hash != nil && elem.Txid != nil && *elem.Txid { - txidsInBumps[elem.Hash.String()] = true + txidsInBumps[*elem.Hash] = true } } } @@ -840,9 +868,8 @@ func (b *Beef) ValidateTransactions() *ValidationResult { } // Maps for tracking - validTxids := make(map[string]bool) - txByID := make(map[string]*BeefTx) - missingInputs := make(map[string]bool) + validTxids := make(map[chainhash.Hash]bool) + missingInputs := make(map[chainhash.Hash]bool) // Lists for processing var hasProof []*BeefTx @@ -852,13 +879,11 @@ func (b *Beef) ValidateTransactions() *ValidationResult { // First pass: categorize transactions for txid, beefTx := range b.Transactions { - txByID[txid] = beefTx - switch beefTx.DataFormat { case TxIDOnly: // TxIDOnly transactions are valid if they appear in BUMPs - if txidsInBumps[txid] { - validTxids[txid] = true + if beefTx.KnownTxID != nil && txidsInBumps[*beefTx.KnownTxID] { + validTxids[*beefTx.KnownTxID] = true txidOnly = append(txidOnly, beefTx) } else { // TxIDOnly without proof - add to txidOnly list but not valid @@ -871,7 +896,7 @@ func (b *Beef) ValidateTransactions() *ValidationResult { // Check if this transaction appears in the specified bump foundInBump := false for _, elem := range bump.Path[0] { - if elem.Hash != nil && elem.Hash.String() == txid { + if elem.Hash != nil && elem.Hash.Equal(txid) { foundInBump = true break } @@ -896,8 +921,8 @@ func (b *Beef) ValidateTransactions() *ValidationResult { // Check if all inputs are available hasMissing := false for _, input := range beefTx.Transaction.Inputs { - if _, exists := b.Transactions[input.SourceTXID.String()]; !exists { - missingInputs[input.SourceTXID.String()] = true + if _, exists := b.Transactions[*input.SourceTXID]; !exists { + missingInputs[*input.SourceTXID] = true hasMissing = true } } @@ -920,7 +945,7 @@ func (b *Beef) ValidateTransactions() *ValidationResult { allInputsValid := true if beefTx.Transaction != nil { for _, input := range beefTx.Transaction.Inputs { - if !validTxids[input.SourceTXID.String()] { + if !validTxids[*input.SourceTXID] { allInputsValid = false break } @@ -928,8 +953,7 @@ func (b *Beef) ValidateTransactions() *ValidationResult { } if allInputsValid { - txid := beefTx.Transaction.TxID().String() - validTxids[txid] = true + validTxids[*beefTx.Transaction.TxID()] = true hasProof = append(hasProof, beefTx) progress = true } else { @@ -960,31 +984,30 @@ func (b *Beef) ValidateTransactions() *ValidationResult { // Add txid-only transactions for _, beefTx := range txidOnly { - var txid string + var txidHash *chainhash.Hash if beefTx.KnownTxID != nil { - txid = beefTx.KnownTxID.String() + txidHash = beefTx.KnownTxID } else if beefTx.Transaction != nil { - txid = beefTx.Transaction.TxID().String() + txidHash = beefTx.Transaction.TxID() } else { continue } - result.TxidOnly = append(result.TxidOnly, txid) - if validTxids[txid] { - result.Valid = append(result.Valid, txid) + result.TxidOnly = append(result.TxidOnly, txidHash.String()) + if validTxids[*txidHash] { + result.Valid = append(result.Valid, txidHash.String()) } } // Add valid transactions with proofs (in dependency order) for _, beefTx := range hasProof { if beefTx.Transaction != nil { - txid := beefTx.Transaction.TxID().String() - result.Valid = append(result.Valid, txid) + result.Valid = append(result.Valid, beefTx.Transaction.TxID().String()) } } // Populate missing inputs list for txid := range missingInputs { - result.MissingInputs = append(result.MissingInputs, txid) + result.MissingInputs = append(result.MissingInputs, txid.String()) } return result @@ -992,10 +1015,10 @@ func (b *Beef) ValidateTransactions() *ValidationResult { func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { r := verifyResult{valid: false, roots: map[uint32]string{}} - + // Validate and sort transactions vr := b.ValidateTransactions() - + // Check if validation passed if len(vr.MissingInputs) > 0 || len(vr.NotValid) > 0 || @@ -1046,7 +1069,7 @@ func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { bump := b.BUMPs[beefTx.BumpIndex] found := false for _, leaf := range bump.Path[0] { - if leaf.Hash != nil && leaf.Hash.String() == txid { + if leaf.Hash != nil && *leaf.Hash == txid { found = true break } @@ -1083,14 +1106,14 @@ txLoop: for i, tx := range b.Transactions { switch tx.DataFormat { case RawTx: - log += fmt.Sprintf(" TX %s\n txid: %s\n", i, tx.Transaction.TxID().String()) + log += fmt.Sprintf(" TX %d\n txid: %s\n", i, tx.Transaction.TxID().String()) log += fmt.Sprintf(" rawTx length=%d\n", len(tx.Transaction.Bytes())) case RawTxAndBumpIndex: - log += fmt.Sprintf(" TX %s\n txid: %s\n", i, tx.Transaction.TxID().String()) + log += fmt.Sprintf(" TX %d\n txid: %s\n", i, tx.Transaction.TxID().String()) log += fmt.Sprintf(" bumpIndex: %d\n", tx.Transaction.MerklePath.BlockHeight) log += fmt.Sprintf(" rawTx length=%d\n", len(tx.Transaction.Bytes())) case TxIDOnly: - log += fmt.Sprintf(" TX %s\n txid: %s\n", i, tx.KnownTxID.String()) + log += fmt.Sprintf(" TX %d\n txid: %s\n", i, tx.KnownTxID.String()) log += " txidOnly\n" continue txLoop } @@ -1117,7 +1140,7 @@ func (b *Beef) Clone() *Beef { c := &Beef{ Version: b.Version, BUMPs: append([]*MerklePath(nil), b.BUMPs...), - Transactions: make(map[string]*BeefTx, len(b.Transactions)), + Transactions: make(map[chainhash.Hash]*BeefTx, len(b.Transactions)), } for k, v := range b.Transactions { c.Transactions[k] = v @@ -1133,7 +1156,7 @@ func (b *Beef) TrimknownTxIDs(knownTxIDs []string) { for txid, tx := range b.Transactions { if tx.DataFormat == TxIDOnly { - if _, ok := knownTxIDSet[txid]; ok { + if _, ok := knownTxIDSet[txid.String()]; ok { delete(b.Transactions, txid) } } @@ -1153,7 +1176,7 @@ func (b *Beef) trimUnreferencedBumps() { usedBumpIndices := make(map[int]bool) // Build a set of transaction IDs that need BUMPs - txidsNeedingBumps := make(map[string]bool) + txidsNeedingBumps := make(map[chainhash.Hash]bool) for txid, tx := range b.Transactions { switch tx.DataFormat { @@ -1166,7 +1189,7 @@ func (b *Beef) trimUnreferencedBumps() { case TxIDOnly: // Known transaction ID - we need to check if any BUMP references this txid if tx.KnownTxID != nil { - txidsNeedingBumps[tx.KnownTxID.String()] = true + txidsNeedingBumps[*tx.KnownTxID] = true } } } @@ -1177,8 +1200,7 @@ func (b *Beef) trimUnreferencedBumps() { // Get the transaction ID from the first path element (leaf level) for _, leaf := range bump.Path[0] { if leaf.Hash != nil { - txidStr := leaf.Hash.String() - if txidsNeedingBumps[txidStr] { + if txidsNeedingBumps[*leaf.Hash] { usedBumpIndices[bumpIndex] = true break } @@ -1265,19 +1287,19 @@ func (b *Beef) Bytes() ([]byte, error) { // transactions / txids beef = append(beef, util.VarInt(len(b.Transactions)).Bytes()...) - txs := make(map[string]struct{}, len(b.Transactions)) + txs := make(map[chainhash.Hash]struct{}, len(b.Transactions)) var appendTx func(tx *BeefTx) error appendTx = func(tx *BeefTx) error { - var txid string + var txid chainhash.Hash if tx.DataFormat == TxIDOnly { if tx.KnownTxID == nil { return fmt.Errorf("txid is nil") } - txid = tx.KnownTxID.String() + txid = *tx.KnownTxID } else if tx.Transaction == nil { return fmt.Errorf("transaction is nil") } else { - txid = tx.Transaction.TxID().String() + txid = *tx.Transaction.TxID() } if _, ok := txs[txid]; ok { return nil @@ -1287,7 +1309,7 @@ func (b *Beef) Bytes() ([]byte, error) { beef = append(beef, tx.KnownTxID[:]...) } else { for _, txin := range tx.Transaction.Inputs { - if parentTx := b.findTxid(txin.SourceTXID.String()); parentTx != nil { + if parentTx := b.findTxid(txin.SourceTXID); parentTx != nil { if err := appendTx(parentTx); err != nil { return err } @@ -1328,9 +1350,9 @@ func (b *Beef) TxidOnly() (*Beef, error) { c := &Beef{ Version: b.Version, BUMPs: append([]*MerklePath(nil), b.BUMPs...), - Transactions: make(map[string]*BeefTx, len(b.Transactions)), + Transactions: make(map[chainhash.Hash]*BeefTx, len(b.Transactions)), } - for i, tx := range b.Transactions { + for txid, tx := range b.Transactions { idOnly := &BeefTx{ DataFormat: TxIDOnly, } @@ -1339,7 +1361,7 @@ func (b *Beef) TxidOnly() (*Beef, error) { } else { idOnly.KnownTxID = tx.Transaction.TxID() } - c.Transactions[i] = idOnly + c.Transactions[txid] = idOnly } return c, nil } diff --git a/transaction/beef_test.go b/transaction/beef_test.go index 40b78032..c593e9ec 100644 --- a/transaction/beef_test.go +++ b/transaction/beef_test.go @@ -38,7 +38,7 @@ func TestFromBEEF(t *testing.T) { txid := tx.TxID() require.Equal(t, expectedTxID, txid.String(), "Transaction ID does not match") - _, err = tx.collectAncestors(txid, map[string]*Transaction{}, true) + _, err = tx.collectAncestors(txid, map[chainhash.Hash]*Transaction{}, true) require.NoError(t, err, "collectAncestors method failed") atomic, err := tx.AtomicBEEF(false) @@ -124,14 +124,14 @@ func TestBeefTransactionFinding(t *testing.T) { // Test RemoveExistingTxid and findTxid for txid := range beef.Transactions { // Verify we can find it - tx := beef.findTxid(txid) + tx := beef.findTxid(&txid) require.NotNil(t, tx) // Remove it - beef.RemoveExistingTxid(txid) + beef.RemoveExistingTxid(&txid) // Verify it's gone - tx = beef.findTxid(txid) + tx = beef.findTxid(&txid) require.Nil(t, tx) break // just test one } @@ -147,7 +147,7 @@ func TestBeefMakeTxidOnly(t *testing.T) { require.NoError(t, err) // Get first transaction and verify it exists - var txid string + var txid chainhash.Hash var originalTx *BeefTx for id, tx := range beef.Transactions { if tx.Transaction != nil { @@ -156,19 +156,15 @@ func TestBeefMakeTxidOnly(t *testing.T) { break } } - require.NotEmpty(t, txid) + require.NotEqual(t, chainhash.Hash{}, txid) require.NotNil(t, originalTx) - // Convert the hash to ensure it's valid - hash, err := chainhash.NewHashFromHex(txid) - require.NoError(t, err) - // Test MakeTxidOnly - txidOnly := beef.MakeTxidOnly(txid) + txidOnly := beef.MakeTxidOnly(&txid) require.NotNil(t, txidOnly) require.Equal(t, TxIDOnly, txidOnly.DataFormat) require.NotNil(t, txidOnly.KnownTxID) - require.Equal(t, hash.String(), txidOnly.KnownTxID.String()) + require.Equal(t, txid.String(), txidOnly.KnownTxID.String()) t.Log(beef.ToLogString()) } @@ -345,8 +341,8 @@ func TestBeefTrimknownTxIDs(t *testing.T) { for txid, tx := range beef.Transactions { if tx.Transaction != nil { // Convert to TxIDOnly and add to our list to trim - beef.MakeTxidOnly(txid) - txidsToTrim = append(txidsToTrim, txid) + beef.MakeTxidOnly(&txid) + txidsToTrim = append(txidsToTrim, txid.String()) if len(txidsToTrim) >= 2 { // Convert 2 transactions to test with break } @@ -356,7 +352,9 @@ func TestBeefTrimknownTxIDs(t *testing.T) { // Verify the transactions are now in TxIDOnly format for _, txid := range txidsToTrim { - tx := beef.findTxid(txid) + hash, err := chainhash.NewHashFromHex(txid) + require.NoError(t, err) + tx := beef.findTxid(hash) require.NotNil(t, tx) require.Equal(t, TxIDOnly, tx.DataFormat) } @@ -366,13 +364,15 @@ func TestBeefTrimknownTxIDs(t *testing.T) { // Verify the transactions were removed for _, txid := range txidsToTrim { - tx := beef.findTxid(txid) + hash, err := chainhash.NewHashFromHex(txid) + require.NoError(t, err) + tx := beef.findTxid(hash) require.Nil(t, tx, "Transaction should have been removed") } // Verify other transactions still exist for txid, tx := range beef.Transactions { - require.NotContains(t, txidsToTrim, txid, "Remaining transaction should not have been in trim list") + require.NotContains(t, txidsToTrim, txid.String(), "Remaining transaction should not have been in trim list") if tx.DataFormat == TxIDOnly { require.NotContains(t, txidsToTrim, txid, "TxIDOnly transaction that wasn't in trim list should still exist") } @@ -423,14 +423,16 @@ func TestBeefGetValidTxids(t *testing.T) { // If we have any valid transactions, verify they exist and have valid inputs if len(validTxids) > 0 { for _, txid := range validTxids { - tx := beef.findTxid(txid) + hash, err := chainhash.NewHashFromHex(txid) + require.NoError(t, err) + tx := beef.findTxid(hash) require.NotNil(t, tx, "Valid txid should exist in transactions map") // If it has a transaction, verify it has no missing inputs // (unless it has a merkle path, in which case it's already proven) if tx.Transaction != nil && tx.Transaction.MerklePath == nil { for _, input := range tx.Transaction.Inputs { - sourceTx := beef.findTxid(input.SourceTXID.String()) + sourceTx := beef.findTxid(input.SourceTXID) require.NotNil(t, sourceTx, "Input transaction should exist for valid transaction without merkle path") } } @@ -482,7 +484,7 @@ func TestBeefFindTransactionForSigning(t *testing.T) { var testTxid string for txid, tx := range beef.Transactions { if tx.Transaction != nil { - testTxid = txid + testTxid = txid.String() break } @@ -508,7 +510,7 @@ func TestBeefFindAtomicTransaction(t *testing.T) { var testTxid string for txid, tx := range beef.Transactions { if tx.Transaction != nil { - testTxid = txid + testTxid = txid.String() break } } @@ -623,7 +625,7 @@ func TestBeefMergeTransactions(t *testing.T) { // Delete this transaction from beef1 to ensure we can merge it delete(beef1.Transactions, id) txToMerge = tx - txid = id + txid = id.String() break } } @@ -641,7 +643,9 @@ func TestBeefMergeTransactions(t *testing.T) { // Test MergeTransaction beef3, err := NewBeefFromBytes(beefBytes) require.NoError(t, err) - delete(beef3.Transactions, txid) + hash, err := chainhash.NewHashFromHex(txid) + require.NoError(t, err) + delete(beef3.Transactions, *hash) initialTxCount = len(beef3.Transactions) beefTx, err = beef3.MergeTransaction(txToMerge.Transaction) require.NoError(t, err) @@ -738,12 +742,12 @@ func TestBeefEdgeCases(t *testing.T) { // Test that TxIDOnly transactions are properly categorized sorted := beef.ValidateTransactions() - require.NotContains(t, sorted.Valid, txid, "TxIDOnly transaction should not be considered valid") - require.Contains(t, sorted.TxidOnly, txid, "TxIDOnly transaction should be in TxidOnly list") + require.NotContains(t, sorted.Valid, txid.String(), "TxIDOnly transaction should not be considered valid") + require.Contains(t, sorted.TxidOnly, txid.String(), "TxIDOnly transaction should be in TxidOnly list") // Test that the transaction is not returned by GetValidTxids validTxids := beef.GetValidTxids() - require.NotContains(t, validTxids, txid, "TxIDOnly transaction should not be in GetValidTxids result") + require.NotContains(t, validTxids, txid.String(), "TxIDOnly transaction should not be in GetValidTxids result") } }) } @@ -812,7 +816,7 @@ func TestBeefMergeBeefTx(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } btx := &BeefTx{ @@ -830,7 +834,7 @@ func TestBeefMergeBeefTx(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Test with nil BeefTx @@ -845,7 +849,7 @@ func TestBeefMergeBeefTx(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Test with BeefTx that has nil Transaction @@ -867,7 +871,7 @@ func TestBeefFindAtomicTransactionWithSourceTransactions(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Create source transaction @@ -881,8 +885,7 @@ func TestBeefFindAtomicTransactionWithSourceTransactions(t *testing.T) { DataFormat: RawTx, Transaction: sourceTx, } - sourceTxid := sourceTx.TxID().String() - beef.Transactions[sourceTxid] = sourceBeefTx + beef.Transactions[*sourceTx.TxID()] = sourceBeefTx // Create main transaction that references the source mainTx := &Transaction{ @@ -903,8 +906,7 @@ func TestBeefFindAtomicTransactionWithSourceTransactions(t *testing.T) { DataFormat: RawTx, Transaction: mainTx, } - mainTxid := mainTx.TxID().String() - beef.Transactions[mainTxid] = mainBeefTx + beef.Transactions[*mainTx.TxID()] = mainBeefTx // Create a BUMP for the source transaction bump := &MerklePath{ @@ -921,6 +923,7 @@ func TestBeefFindAtomicTransactionWithSourceTransactions(t *testing.T) { beef.BUMPs = append(beef.BUMPs, bump) // Test FindAtomicTransaction + mainTxid := mainTx.TxID().String() result := beef.FindAtomicTransaction(mainTxid) require.NotNil(t, result) require.Equal(t, mainTxid, result.TxID().String()) @@ -935,7 +938,7 @@ func TestBeefMergeTxidOnly(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Create a transaction ID @@ -945,7 +948,7 @@ func TestBeefMergeTxidOnly(t *testing.T) { require.NoError(t, err) // Test MergeTxidOnly - result := beef.MergeTxidOnly(txid.String()) + result := beef.MergeTxidOnly(txid) require.NotNil(t, result) require.Equal(t, TxIDOnly, result.DataFormat) require.NotNil(t, result.KnownTxID) @@ -954,10 +957,10 @@ func TestBeefMergeTxidOnly(t *testing.T) { // Verify the transaction was added to the BEEF object require.Len(t, beef.Transactions, 1) - require.Contains(t, beef.Transactions, txid.String()) + require.Contains(t, beef.Transactions, *txid) // Test merging the same txid again - result2 := beef.MergeTxidOnly(txid.String()) + result2 := beef.MergeTxidOnly(txid) require.NotNil(t, result2) require.Equal(t, result, result2) require.Len(t, beef.Transactions, 1) @@ -968,7 +971,7 @@ func TestBeefFindBumpWithNilBumpIndex(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Create a transaction with a source transaction @@ -995,11 +998,11 @@ func TestBeefFindBumpWithNilBumpIndex(t *testing.T) { } // Add transactions to BEEF - beef.Transactions[sourceTx.TxID().String()] = &BeefTx{ + beef.Transactions[*sourceTx.TxID()] = &BeefTx{ DataFormat: RawTx, Transaction: sourceTx, } - beef.Transactions[mainTx.TxID().String()] = &BeefTx{ + beef.Transactions[*mainTx.TxID()] = &BeefTx{ DataFormat: RawTx, Transaction: mainTx, } @@ -1019,7 +1022,7 @@ func TestBeefBytes(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Add a TxIDOnly transaction @@ -1027,7 +1030,7 @@ func TestBeefBytes(t *testing.T) { require.NoError(t, err) txid, err := chainhash.NewHash(txidBytes) require.NoError(t, err) - beef.MergeTxidOnly(txid.String()) + beef.MergeTxidOnly(txid) // Add a RawTx transaction tx := &Transaction{ @@ -1092,7 +1095,7 @@ func TestBeefAddComputedLeaves(t *testing.T) { beef := &Beef{ Version: BEEF_V2, BUMPs: make([]*MerklePath, 0), - Transactions: make(map[string]*BeefTx), + Transactions: make(map[chainhash.Hash]*BeefTx), } // Create leaf hashes @@ -1253,8 +1256,10 @@ func TestMakeTxidOnlyAndBytes(t *testing.T) { require.NoError(t, err) knownTxID := "b1fc0f44ba629dbdffab9e34fcc4faf9dbde3560a7365c55c26fe4daab052aac" + hash, err := chainhash.NewHashFromHex(knownTxID) + require.NoError(t, err) - beef.MakeTxidOnly(knownTxID) + beef.MakeTxidOnly(hash) _, err = beef.Bytes() // <--------- it panics here require.NoError(t, err) diff --git a/transaction/transaction.go b/transaction/transaction.go index ad662926..a126f542 100644 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -519,7 +519,7 @@ func (t *Transaction) AtomicBEEF(allowPartial bool) ([]byte, error) { bumps := []*MerklePath{} bumpMap := map[uint32]int{} txid := t.TxID() - txns := map[string]*Transaction{txid.String(): t} + txns := map[chainhash.Hash]*Transaction{*txid: t} ancestors, err := t.collectAncestors(txid, txns, allowPartial) if err != nil { return nil, err From efd964e8eaf1982d7d610387e06025b25fc6f243 Mon Sep 17 00:00:00 2001 From: David Case Date: Fri, 18 Jul 2025 11:17:19 -0400 Subject: [PATCH 6/9] remove comment --- transaction/beef_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transaction/beef_test.go b/transaction/beef_test.go index c593e9ec..999798ab 100644 --- a/transaction/beef_test.go +++ b/transaction/beef_test.go @@ -1261,7 +1261,7 @@ func TestMakeTxidOnlyAndBytes(t *testing.T) { beef.MakeTxidOnly(hash) - _, err = beef.Bytes() // <--------- it panics here + _, err = beef.Bytes() require.NoError(t, err) _ = beef.ToLogString() } From aec4025ca16c08e801ca9916661c1e2367b0df60 Mon Sep 17 00:00:00 2001 From: David Case Date: Mon, 21 Jul 2025 12:39:16 -0400 Subject: [PATCH 7/9] update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1fd36c9..8128dead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format ## Table of Contents -- [1.2.6 - 2025-07-17](#126---2025-07-17) +- [1.2.6 - 2025-07-21](#126---2025-07-21) - [1.2.5 - 2025-07-16](#125---2025-07-16) - [1.2.4 - 2025-06-30](#124---2025-06-30) - [1.2.3 - 2025-06-30](#123---2025-06-30) @@ -41,7 +41,7 @@ All notable changes to this project will be documented in this file. The format - [1.1.0 - 2024-08-19](#110---2024-08-19) - [1.0.0 - 2024-06-06](#100---2024-06-06) -## [1.2.6] - 2025-07-17 +## [1.2.6] - 2025-07-21 ### Fixed - Fixed BEEF validation stability issue where `IsValid` returned inconsistent results (#211) From 061f4c6cec96ce5f726a0ae29f9b2249d20c8417 Mon Sep 17 00:00:00 2001 From: David Case Date: Mon, 21 Jul 2025 09:42:00 -0700 Subject: [PATCH 8/9] Update transaction/beef.go Co-authored-by: chris-4chain <152964795+chris-4chain@users.noreply.github.com> --- transaction/beef.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transaction/beef.go b/transaction/beef.go index 09717903..0afe95b4 100644 --- a/transaction/beef.go +++ b/transaction/beef.go @@ -983,6 +983,7 @@ func (b *Beef) ValidateTransactions() *ValidationResult { } // Add txid-only transactions + result.TxidOnly = make([]string, 0, len(txidOnly)) for _, beefTx := range txidOnly { var txidHash *chainhash.Hash if beefTx.KnownTxID != nil { From 6b6da17e49fa33c16d4ce6f45b06212aa06eed27 Mon Sep 17 00:00:00 2001 From: David Case Date: Mon, 21 Jul 2025 09:42:10 -0700 Subject: [PATCH 9/9] Update transaction/beef.go Co-authored-by: chris-4chain <152964795+chris-4chain@users.noreply.github.com> --- transaction/beef.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transaction/beef.go b/transaction/beef.go index 0afe95b4..ba5c6d2b 100644 --- a/transaction/beef.go +++ b/transaction/beef.go @@ -1007,6 +1007,7 @@ func (b *Beef) ValidateTransactions() *ValidationResult { } // Populate missing inputs list + result.MissingInputs = make([]string, 0, len(missingInputs)) for txid := range missingInputs { result.MissingInputs = append(result.MissingInputs, txid.String()) }