diff --git a/transaction/beef.go b/transaction/beef.go index 8cf582b..0d9e076 100644 --- a/transaction/beef.go +++ b/transaction/beef.go @@ -7,10 +7,13 @@ import ( "fmt" "github.com/bitcoin-sv/go-sdk/chainhash" + "github.com/bitcoin-sv/go-sdk/transaction/chaintracker" ) -// Beef is a set of transactions and their MerklePaths without a strict relationship between each transaction. -// It's useful when syncing multiple transactions all at once, and a txid can be used in the case that the recipient already knows a particular transaction. +// Beef is a set of Transactions and their MerklePaths. +// Each Transaction can be RawTx, RawTxAndBumpIndex, or TxIDOnly. +// It's useful when transporting multiple transactions all at once. +// Txid only can be used in the case that the recipient already has that tx. type Beef struct { Version uint32 BUMPs []*MerklePath @@ -71,7 +74,7 @@ func NewBEEFFromBytes(beef []byte) (*Beef, error) { return &Beef{ Version: version, BUMPs: BUMPs, - Transactions: txs, + Transactions: *txs, }, nil } @@ -186,8 +189,9 @@ func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[string]*BeefTx, if err != nil { return nil, err } - var beefTx *BeefTx - .dataFormat = DataFormat(formatByte) + var beefTx BeefTx + beefTx.dataFormat = DataFormat(formatByte) + beefTx.Transaction = &Transaction{} if beefTx.dataFormat > TxIDOnly { return nil, fmt.Errorf("invalid data format: %d", formatByte) @@ -200,7 +204,7 @@ func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[string]*BeefTx, if err != nil { return nil, err } - txs[txid.String()] = beefTx + txs[txid.String()] = &beefTx } else { bump := beefTx.dataFormat == RawTxAndBumpIndex // read the index of the bump @@ -230,7 +234,7 @@ func readBeefTx(reader *bytes.Reader, BUMPs []*MerklePath) (*map[string]*BeefTx, } } - txs[beefTx.Transaction.TxID().String()] = beefTx + txs[beefTx.Transaction.TxID().String()] = &beefTx } } @@ -248,7 +252,7 @@ func NewTransactionFromBEEFHex(beefHex string) (*Transaction, error) { func (t *Transaction) BEEF() ([]byte, error) { b := new(bytes.Buffer) - err := binary.Write(b, binary.LittleEndian, BEEF_VERSION) + err := binary.Write(b, binary.LittleEndian, BEEF_V1) if err != nil { return nil, err } @@ -323,3 +327,458 @@ func (t *Transaction) collectAncestors(txns map[string]*Transaction) ([]string, ancestors = append(ancestors, t.TxID().String()) return ancestors, nil } + +func (b *Beef) findTxid(txid string) *BeefTx { + if tx, ok := b.Transactions[txid]; ok { + return tx + } + return nil +} + +func (b *Beef) makeTxidOnly(txid string) *BeefTx { + tx, ok := b.Transactions[txid] + if !ok { + return nil + } + if tx.dataFormat == TxIDOnly { + return tx + } + delete(b.Transactions, txid) + tx = &BeefTx{ + dataFormat: TxIDOnly, + KnownTxID: tx.KnownTxID, + } + b.Transactions[txid] = tx + return tx +} + +func (b *Beef) findBump(txid string) *MerklePath { + for _, bump := range b.BUMPs { + for _, leaf := range bump.Path[0] { + if leaf.Hash.String() == txid { + return bump + } + } + } + return nil +} + +func (b *Beef) findTransactionForSigning(txid string) *Transaction { + beefTx := b.findTxid(txid) + if beefTx == nil { + return nil + } + + for _, input := range beefTx.Transaction.Inputs { + if input.SourceTransaction == nil { + itx := b.findTxid(input.SourceTXID.String()) + if itx != nil { + input.SourceTransaction = itx.Transaction + } + } + } + + return beefTx.Transaction +} + +func (b *Beef) findAtomicTransaction(txid string) *Transaction { + beefTx := b.findTxid(txid) + if beefTx == nil { + return nil + } + + var addInputProof func(beef *Beef, tx *Transaction) + addInputProof = func(beef *Beef, tx *Transaction) { + mp := beef.findBump(tx.TxID().String()) + if mp != nil { + tx.MerklePath = mp + } else { + for _, input := range tx.Inputs { + if input.SourceTransaction == nil { + itx := beef.findTxid(input.SourceTXID.String()) + if itx != nil { + input.SourceTransaction = itx.Transaction + } + } + if input.SourceTransaction != nil { + mp := beef.findBump(input.SourceTransaction.TxID().String()) + if mp != nil { + input.SourceTransaction.MerklePath = mp + } else { + addInputProof(beef, input.SourceTransaction) + } + } + } + } + } + + addInputProof(b, beefTx.Transaction) + + return beefTx.Transaction +} + +func (b *Beef) mergeBump(bump *MerklePath) int { + var bumpIndex *int + // If this proof is identical to another one previously added, we use that first. Otherwise, we try to merge it with proofs from the same block. + for i, existingBump := range b.BUMPs { + if existingBump == bump { // Literally the same + return i + } + if existingBump.BlockHeight == bump.BlockHeight { + // Probably the same... + rootA, err := existingBump.ComputeRoot(nil) + if err != nil { + return -1 + } + rootB, err := bump.ComputeRoot(nil) + if err != nil { + return -1 + } + if rootA == rootB { + // Definitely the same... combine them to save space + existingBump.Combine(bump) + bumpIndex = &i + break + } + } + } + + // if the proof is not yet added, add a new path. + if bumpIndex == nil { + newIndex := len(b.BUMPs) + b.BUMPs = append(b.BUMPs, bump) + bumpIndex = &newIndex + } + + // review if any transactions are proven by this bump + for _, tx := range b.Transactions { + txid := tx.Transaction.TxID().String() + if tx.Transaction.MerklePath == nil { + for _, node := range b.BUMPs[*bumpIndex].Path[0] { + if node.Hash.String() == txid { + tx.Transaction.MerklePath = b.BUMPs[*bumpIndex] + break + } + } + } + } + + return *bumpIndex +} + +func (b *Beef) mergeRawTx(rawTx []byte, bumpIndex *int) (*BeefTx, error) { + tx := &Transaction{} + reader := bytes.NewReader(rawTx) + _, err := tx.ReadFrom(reader) + if err != nil { + return nil, err + } + + txid := tx.TxID().String() + b.removeExistingTxid(txid) + + beefTx := &BeefTx{ + dataFormat: RawTx, + Transaction: tx, + } + + if bumpIndex != nil { + if *bumpIndex < 0 || *bumpIndex >= len(b.BUMPs) { + return nil, fmt.Errorf("invalid bump index") + } + beefTx.Transaction.MerklePath = b.BUMPs[*bumpIndex] + beefTx.dataFormat = RawTxAndBumpIndex + } + + 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) tryToValidateBumpIndex(tx *BeefTx) { + if tx.Transaction.MerklePath == nil { + return + } + for _, node := range tx.Transaction.MerklePath.Path[0] { + if node.Hash.String() == tx.Transaction.TxID().String() { + return + } + } + tx.Transaction.MerklePath = nil +} + +func (b *Beef) mergeTransaction(tx *Transaction) (*BeefTx, error) { + txid := tx.TxID().String() + b.removeExistingTxid(txid) + + var bumpIndex *int + if tx.MerklePath != nil { + index := b.mergeBump(tx.MerklePath) + bumpIndex = &index + } + + newTx := &BeefTx{ + dataFormat: RawTx, + Transaction: tx, + } + if bumpIndex != nil { + newTx.dataFormat = RawTxAndBumpIndex + } + + b.Transactions[txid] = newTx + b.tryToValidateBumpIndex(newTx) + + if bumpIndex == nil { + for _, input := range tx.Inputs { + if input.SourceTransaction != nil { + if _, err := b.mergeTransaction(input.SourceTransaction); err != nil { + return nil, err + } + } + } + } + + return newTx, nil +} + +func (b *Beef) mergeTxidOnly(txid string) *BeefTx { + tx := b.findTxid(txid) + if tx == nil { + tx = &BeefTx{ + dataFormat: TxIDOnly, + KnownTxID: &chainhash.Hash{}, + } + copy(tx.KnownTxID[:], txid) + b.Transactions[txid] = tx + b.tryToValidateBumpIndex(tx) + } + return tx +} + +func (b *Beef) mergeBeefTx(btx *BeefTx) (*BeefTx, error) { + beefTx := b.findTxid(btx.Transaction.TxID().String()) + if btx.dataFormat == TxIDOnly && beefTx == nil { + beefTx = b.mergeTxidOnly(btx.KnownTxID.String()) + } else if btx.Transaction != nil && (beefTx == nil || beefTx.dataFormat == TxIDOnly) { + var err error + beefTx, err = b.mergeTransaction(btx.Transaction) + if err != nil { + return nil, err + } + } else if btx.Transaction != nil && (beefTx == nil || beefTx.dataFormat == TxIDOnly) { + var err error + beefTx, err = b.mergeRawTx(btx.Transaction.Bytes(), nil) + if err != nil { + return nil, err + } + } + return beefTx, nil +} + +func (b *Beef) mergeBeefBytes(beef []byte) error { + otherBeef, err := NewBEEFFromBytes(beef) + if err != nil { + return err + } + return b.mergeBeef(otherBeef) +} + +func (b *Beef) mergeBeef(otherBeef *Beef) error { + for _, bump := range otherBeef.BUMPs { + b.mergeBump(bump) + } + + for _, tx := range otherBeef.Transactions { + if _, err := b.mergeBeefTx(tx); err != nil { + return err + } + } + + return nil +} + +type verifyResult struct { + valid bool + roots map[uint32]string +} + +func (b *Beef) IsValid(allowTxidOnly bool) bool { + r := b.verifyValid(allowTxidOnly) + return r.valid +} + +func (b *Beef) Verify(chainTracker chaintracker.ChainTracker, allowTxidOnly bool) (bool, error) { + r := b.verifyValid(allowTxidOnly) + if !r.valid { + return false, nil + } + for height, root := range r.roots { + h, err := chainhash.NewHashFromHex(root) + if err != nil { + return false, err + } + ok, err := chainTracker.IsValidRootForHeight(h, height) + if err != nil || !ok { + return false, err + } + } + return true, nil +} + +func (b *Beef) SortTxs() 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{} + + // 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{} + + 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 + } + } + + // 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 + } + } + if hasMissing { + res.WithMissingInputs = append(res.WithMissingInputs, beefTx.Transaction.TxID().String()) + } else { + queue = append(queue, 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 { + if beefTx.Transaction != nil { + allInputsValid := true + for _, in := range beefTx.Transaction.Inputs { + if !validTxids[in.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) + } + } + } + queue = newQueue + } + + // Now, whatever is left in queue is not valid + for _, beefTx := range queue { + if beefTx.Transaction != nil { + res.NotValid = append(res.NotValid, beefTx.Transaction.TxID().String()) + } + } + + for k := range missing { + res.MissingInputs = append(res.MissingInputs, k) + } + return struct { + MissingInputs []string + NotValid []string + Valid []string + WithMissingInputs []string + TxidOnly []string + }(res) +} + +func (b *Beef) verifyValid(allowTxidOnly bool) verifyResult { + r := verifyResult{valid: false, roots: map[uint32]string{}} + b.sortTxs() // Assume this sorts transactions in dependency order + + txids := make(map[string]bool) + for _, tx := range b.Transactions { + if tx.dataFormat == TxIDOnly { + if !allowTxidOnly { + return r + } + txids[tx.KnownTxID.String()] = true + } + } + + confirmComputedRoot := func(mp *MerklePath, txid string) bool { + h, err := chainhash.NewHashFromHex(txid) + if err != nil { + return false + } + root, err := mp.ComputeRoot(h) + if err != nil { + return false + } + if existing, ok := r.roots[mp.BlockHeight]; ok && existing != root.String() { + return false + } + r.roots[mp.BlockHeight] = root.String() + return true + } + + for _, mp := range b.BUMPs { + for _, n := range mp.Path[0] { + if n.Txid != nil && n.Hash != nil { + if !confirmComputedRoot(mp, n.Hash.String()) { + return r + } + txids[n.Hash.String()] = true + } + } + } + + for txid, beefTx := range b.Transactions { + if beefTx.dataFormat != TxIDOnly { + for _, in := range beefTx.Transaction.Inputs { + if !txids[in.SourceTXID.String()] { + return r + } + } + } + txids[txid] = true + } + + r.valid = true + return r +} diff --git a/transaction/beef_test.go b/transaction/beef_test.go index adf542c..c279fd2 100644 --- a/transaction/beef_test.go +++ b/transaction/beef_test.go @@ -4,6 +4,7 @@ package transaction import ( "encoding/base64" + "encoding/hex" "testing" "github.com/stretchr/testify/require" @@ -11,6 +12,7 @@ import ( const BRC62Hex = "0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000" const BEEF = "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" +const BEEFSet = "0200beef03fef1550d001102fd20c2009591fd79f7fb1fbd24c2fdc4911da930e1d7386f0216b6446b85eea29f978f1bfd21c202ac2a05abdae46fc2555c36a76035dedbf9fac4fc349eabffbd9d62ba440ffcb101fd116100cabeb714ea9a3f15a5e4f6138f6dd6b75bab32d8b40d178a0514e6e1e1b372f701fd8930007e04df7216a1d29bb8caabd1f78014b1b4f336eb6aee76bcf1797456ddc86b7501fd451800796afe5b113d8933f5eef2d180e72dc4b644fd76fb1243dfb791d9863702573701fd230c007a6edc003e02c429391cbf426816885731cb8054410599884eed508917a2f57c01fd100600eaa540de74506ed6abcb48e38cc544c53d373269271a7e6cf2143b7cc85d7ea401fd0903001e31aa04628b99d6cfa3e21fb4a7e773487ebc86a504e511eaff3f2176267b9401fd85010031e0d053497f85228b02879f69c4c7b43fb5abc3e0e47ea49a63853b117c9b5001c30083339d5a5b97ad77b74d3538678bb20ea7e61f8b02c24a625933eb496bebd3480160008ee445baec1613d591344a9915d77652f508e6442cd394626a3ff308bcb151f1013100f3f68f2a72e47bb41377e9e429daa496cd220bdcf702a36a209f9feba58d5552011900a01c52f4099bc7bdfea772ab03739bf009d72f24f68b5c4f8cc71a8c4da80804010d00c2ce2d5bfb9cbab9983ae1c871974f23a32c585d9b8440acc4ef5203c1d6c05401070072c7fc59a1717e90633f10d322e0f63272ae97c017d1efae04e4090abeeafac3010200a7aa5fa5576d1de6dd0e32d769592bc247be7bbd0b3e36e2d579fa1ec7d6ebce010000090cba670bea2e0d5c36e979e4cf9f79ad0874d734fb782fec2496d4c554e321010100d963646680643df73c34d7fa16f173595cf32a9ed6f64d2c8ee88a8af6b7bf52fedf590d001202fe66130200023275c6dde10d32d61af52b412b1e3956b5cd085605cd521778f11d53849fdb0cfe6713020000cd5e2298cf4d809c698c8adeeab66718e6b75b3d528bce74e6e01b984c736df901feb209010000736013454e087c89d813c99a043c9029cf2d427815c6a98ba3641c384ae52c4701fdd884007f742824bddca1582e4ded866d9609d9473397f8b86625376be74684f7fb947f01fd6d4200eb7f54ce4f920a3e4c7f96ef6b2d199c519df1b1286415581187ca608f3e47b801fd372100fa6c1c8cba3d3d5d030cd98eb91498cdffe70f0dad1000e123157d5dac22e22a01fd9a1000104c0294e478fbcac4e2325403afd86370c86043f295978b809004b2687a6c9a01fd4c08009ef5a5eaf16cab45a239c43852296ab323ca21faf256ab9768dd0a2f39970ec201fd2704006161cbd1755b66815eb69613b574920e9e836c8c3772aa2260ad3639848d520b01fd1202005e04b5afc0ea8d29dc22b611536832a2a2e7c860bbf4227ce0bdcc8a0e66284601fd0801009719f5f90e3937f3921045d202522fe315da1331acc3cce472c4b084d0debe65018500d79a1c3d45a3c41bf6526a9adbac2676159d2f3c753d7d3b6dba1dc3cbdd3c520143006b88b582d985bffc511556e471a6a20cfda2d41837245329f714214e009a3e48012000c1840dbdfc3014f1e912882b971c030fd21c0b023c01fe6fd7470d6d9bb2ab86011100f9c3de08d38588e225a5ee5334a3c03771a0b51318ca388dd1b5826951604d750109006e2b2e926c86214620d306a59522eee438a79157e9360cb76ee14a868fccc482010500d5c43ea372c432861db73ba0a6897fa29855e542a6ed910626dfb8954d94fa47010300d7863bafb5ca841ca0b13736fced1d492f0f741cb0a2beab1cafa517c878ae2c010000174ccda0879c20b85fa26d423deb0b34c5f2787127e244ccacfae39b5ba8fea7feeb590d001602fe46b3060002fa6ae8371111956f74412e3b1effcbd4fcb278124b6365b34c8cc20a5287bafffe47b306000011883eed76bdc7e7fb79efe23e3c50aa825ade46d79895de1a246e3d69a5b8cf01fea2590300009c92d7f67ac06e4bce0de4f18f438056f25138ee1a0cf61ed3a6d7f32261339b01fed0ac01000006178026214d61dc19c91cb5c08481f2f3daf03392c359de424cbd5d7135c5cf01fd69d6000174f6863438909d648fea32cdd65cbf457ab717f9be327d5d4352dbf157671e01fd356b0059536ea55010906b7071e36f78b20faaaede46a7f27ba4916dc1655836c73de701fd9b3500dee845c02c827dbcd862de359f5e6ad0ecca59213d9eb01896374d9efb7af9fd01fdcc1a00b22861b84b4537dfdaa8eb51957a51007af7836677ad14074601de6cd6c2871c01fd670d00591e76e7b07b26a6d7e940ec4f84497d9f3c7be111b15c336b24d83227db0c1001fdb20600f142d0ff9b2ddb7c21d8913f02adc7abc51fcdd5253154339450b87b59859aa601fd580300ce0307ff2027d405b8afa8a5c8834e9cc8bd073c4f463c3657562bbdb7843fe601fdad010027a3ce3a9829a3df0d9074099a6a3d76c81600a6a9c50f6cf857fb823c1a783901d700cca7689680c528f0a93fd9c980577016b37ce67ce75b1d728c4fa23008b1652b016a00b74bd3ab6c94f1216a803849afc254f37eea378c89167ff0686223db82767e3a013400434d5f48f733bb69fc5f0bd8238ffaec8d002951e6a1b52484fcc05819078372011b0053fef8153f4aed8aa8bdebeae0a6c1aa7712b84887fb565bcd9232fdd60fb0c0010c00009d9f21a9bc9e9d8c99aac9a1df47ffe02334fcb8bc8f3797d64c2564b3bf44010700838a284a4ee33c455b303e1eb23428b35d264b35c4f4b42bd6c68f1a7279f38801020042820e1ab5dbb77b0a6f266167b453f672d007d0c6eddc6229ce57c941f46c670100002c0da37e0453e7d01c810d2280a84792086b1fe1bc232e76ef6783f76c57757601010048746ad4d10a562bb53d2ed29438c9dfd0a6cacb78429277072e789d4d8dd8c101010091a52bf4a100e96dba15cbff933df60fcb26d95d6dd9b55fd5e450d5895e4526010100c202dcbdece72a45a1657ff7dbd979b031b1c8b839bc9a3b958683226644b736030100020000000140f6726035b03b90c1f770f0280444eeb041c45d026a8f4baaf00530bdc473a5020000006b483045022100ccdf467aa46d9570c4778f4e68491cc51dff4b815803d2406b6e8772d800f5ad02200ff8f11a59d207c734e9c68154dcef4023d75c37e661ab866b1d3e3ea77e6bda4121021cf99b6763736f48e6e063f99a43bfa82f15111ba0e0f9776280e6bd75d23af9ffffffff0377082800000000001976a91491b21f8856b862ff291ca0ac2ec924ba2419113788ac75330100000000001976a9144b5b285395052a61328b58c6594dd66aa6003d4988acf229f503000000001976a9148efcb6c55f5c299d48d0c74762dd811345c9093b88ac0000000001010200000001bcfe1adc5e99edb82c6a48f44cbae19bc0e5d31f9c8e4b3a92d6befb1cb2e510020000006a4730440220211655b505edd6fe9196aba77477dac5c9f638fe204243c09f1188a19164ac7f022035fb8640750515ca85df8197dec87a76db5c578f05b8ae645e30d8f70d429a324121028bf1be8161c50f98289df3ecd3185ed2273e9d448840232cf2f077f05e789c29ffffffff03d8000400000000001976a9144f427ee5f3099f0ac571f6b723a628e7b08fb64c88ac75330100000000001976a914f7cad87036406e5d3aef5d4a4d65887c76f9466788ac27db1004000000001976a9143219d1b6bd74f932dcb39a5f3b48cfde2b61cc0088ac0000000001020100000002e646efa607ff14299bc0b0cfaa65e035feb493cc440cb8abb8eb6225f8d4c1c4000000006b483045022100b410c4f82655f56fc8de4a622d3e4a8c662198de5ca8963989d70b85734986f502204fe884d99aa6ffd44bb01396b9f63bebcb7222b76e6e26c2bd60837ff555f1f8412103fda4ece7b0c9150872f8ef5241164b36a230fd9657bc43ca083d9e78bc0bcba6ffffffff3275c6dde10d32d61af52b412b1e3956b5cd085605cd521778f11d53849fdb0c000000006a473044022057f9d55ace1945866be0f83431867c58eda32d73ae3fdabed2d3424ebbe493530220553e286ae67bcaf49b0ea1d3163f41b1b3c91702a054e100c1e71ca4927f6dd8412103fda4ece7b0c9150872f8ef5241164b36a230fd9657bc43ca083d9e78bc0bcba6ffffffff04400d0300000000001976a9140e8338fa60e5391d54e99c734640e72461922d9988aca0860100000000001976a9140602787cc457f68c43581224fda6b9555aaab58e88ac10270000000000001976a91402cfbfc3931c7c1cf712574e80e75b1c2df14b2088acd5120000000000001976a914bd3dbab46060873e17ca754b0db0da4552c9a09388ac00000000" func TestFromBEEF(t *testing.T) { // Decode the BEEF data from base64 @@ -28,3 +30,18 @@ func TestFromBEEF(t *testing.T) { actualTxID := tx.TxID().String() require.Equal(t, expectedTxID, actualTxID, "Transaction ID does not match") } + +func TestNewBEEFFromBytes(t *testing.T) { + // Decode the BEEF data from base64 + beefBytes, err := hex.DecodeString(BEEFSet) + require.NoError(t, err, "Failed to decode BEEF data from hex string") + + // Create a new Beef object + beef, err := NewBEEFFromBytes(beefBytes) + require.NoError(t, err, "NewBEEFFromBytes method failed") + + // Check the Beef object's properties + require.Equal(t, uint32(4022206466), beef.Version, "Version does not match") + require.Len(t, beef.BUMPs, 3, "BUMPs length does not match") + require.Equal(t, 3, len(beef.Transactions), "Transactions length does not match") +}