From e34c2738d06d49cdbd3c00f3da164704a7d31caf Mon Sep 17 00:00:00 2001 From: Anh Minh <1phamminh0811@gmail.com> Date: Thu, 16 Oct 2025 12:31:21 +0700 Subject: [PATCH] fix: send tx with 2 notes --- wallet/nockhash.go | 31 ++++++--- wallet/service.go | 76 ++++++++++++++++------ wallet/service_test.go | 40 ++++++++++++ wallet/ztree.go | 140 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 wallet/ztree.go diff --git a/wallet/nockhash.go b/wallet/nockhash.go index e08dd81..ac50671 100644 --- a/wallet/nockhash.go +++ b/wallet/nockhash.go @@ -388,7 +388,6 @@ func HashMsg(spend *nockchain.NockchainSpend) ([5]uint64, error) { finalSeedHash = crypto.Tip5RehashTenCell(seed1Hash, crypto.Tip5ZeroZero) finalSeedHash = crypto.Tip5RehashTenCell(crypto.Tip5Zero, finalSeedHash) finalSeedHash = crypto.Tip5RehashTenCell(seed2Hash, finalSeedHash) - } } } @@ -410,16 +409,34 @@ func HashInput(input *nockchain.NockchainInput) ([5]uint64, error) { } hashNoteSpend := crypto.Tip5RehashTenCell(noteHash, spendHash) - inputHash := crypto.Tip5RehashTenCell(nameHash, hashNoteSpend) - return crypto.Tip5RehashTenCell(inputHash, crypto.Tip5ZeroZero), nil + return crypto.Tip5RehashTenCell(nameHash, hashNoteSpend), nil } func ComputeTxId(inputs []*nockchain.NockchainInput, timelockRange *nockchain.TimelockRange, totalFees uint64) ([5]uint64, error) { - // TODO: do it with multiple intputs - input := inputs[0] - inputHash, err := HashInput(input) + + inputTree := NewZTree( + func(i interface{}) [5]uint64 { + if name, ok := i.(*nockchain.NockchainName); ok { + return HashName(name) + } else { + return [5]uint64{} + } + }, + func(i interface{}) ([5]uint64, error) { + if input, ok := i.(*nockchain.NockchainInput); ok { + hash, err := HashInput(input) + return hash, err + } else { + return [5]uint64{}, fmt.Errorf("invalid input type") + } + }, + ) + for _, input := range inputs { + inputTree.Insert(input.Name, input) + } + inputHash, err := inputTree.Hash() if err != nil { - return [5]uint64{}, err + return [5]uint64{}, fmt.Errorf("error hashing inputs: %v", err) } timelockHash := HashTimelockRange(timelockRange) diff --git a/wallet/service.go b/wallet/service.go index cdcf7ab..7ec260d 100644 --- a/wallet/service.go +++ b/wallet/service.go @@ -267,8 +267,7 @@ func (h *GprcHandler) DeriveChild(ctx context.Context, req *nockchain.DeriveChil // // - `fee` - Transaction fee to be subtracted from one of the input notes func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxRequest) (*nockchain.CreateTxResponse, error) { - firstNames := [][5]uint64{} - lastNames := [][5]uint64{} + nnames := []*nockchain.NockchainName{} names := strings.Split(req.Names, ",") notes := make([]*nockchain.NockchainNote, len(names)) for _, name := range names { @@ -277,12 +276,38 @@ func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxReque inner := name[1 : len(name)-1] part := strings.Split(inner, " ") if len(part) == 2 { - firstNames = append(firstNames, crypto.Base58ToTip5Hash(part[0])) - lastNames = append(lastNames, crypto.Base58ToTip5Hash(part[1])) + nnames = append(nnames, &nockchain.NockchainName{ + First: part[0], + Last: part[1], + }) } } } + nameTree := NewZTree( + func(i interface{}) [5]uint64 { + if name, ok := i.(*nockchain.NockchainName); ok { + return HashName(name) + } else { + return [5]uint64{} + } + }, + nil, + ) + for _, name := range nnames { + nameTree.Insert(name, nil) + } + nameLeftMost := nameTree.KeyLeftMost().(*nockchain.NockchainName) + idxLeftMost := -1 + for i, name := range nnames { + if name.First == nameLeftMost.First && name.Last == nameLeftMost.Last { + idxLeftMost = i + } + } + if idxLeftMost == -1 { + return nil, fmt.Errorf("unable to find left most node") + } + recipents := []*nockchain.NockchainLock{} if strings.Contains(req.Recipients, "[") { pairs := strings.Split(req.Recipients, ",") @@ -329,7 +354,7 @@ func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxReque // No additional validation needed - any number of names is allowed } else { // Multiple mode: all lengths must match - if len(firstNames) != len(recipents) || len(firstNames) != len(gifts) { + if len(nnames) != len(recipents) || len(nnames) != len(gifts) { return nil, fmt.Errorf("multiple recipient mode requires names, recipients, and gifts to have the same length") } } @@ -409,8 +434,18 @@ func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxReque ParentHash: crypto.Tip5HashToBase58(parentHash), }, } - - if notes[i].Asset < gifts[i]+req.Fee { + if notes[i].Asset < gifts[i] { + return nil, fmt.Errorf("insufficient funds for notes %s", names[i]) + } + assetLeft := notes[i].Asset - gifts[i] + if i == idxLeftMost { + if assetLeft > req.Fee { + assetLeft -= req.Fee + } else { + return nil, fmt.Errorf("insufficient funds for notes %s", names[i]) + } + } + if assetLeft != 0 { seeds = append(seeds, &nockchain.NockchainSeed{ OutputSource: nil, Recipient: &nockchain.NockchainLock{ @@ -418,15 +453,23 @@ func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxReque Pubkeys: []string{base58.Encode(masterKey.PublicKey)}, }, TimelockIntent: req.TimelockIntent, - Gift: notes[i].Asset - gifts[i] - req.Fee, + Gift: assetLeft, ParentHash: crypto.Tip5HashToBase58(parentHash), }) } - - spend := nockchain.NockchainSpend{ - Signatures: nil, - Seeds: seeds, - Fee: req.Fee, + var spend nockchain.NockchainSpend + if i == idxLeftMost { + spend = nockchain.NockchainSpend{ + Signatures: nil, + Seeds: seeds, + Fee: req.Fee, + } + } else { + spend = nockchain.NockchainSpend{ + Signatures: nil, + Seeds: seeds, + Fee: 0, + } } msg, err := HashMsg(&spend) @@ -449,10 +492,7 @@ func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxReque } input := nockchain.NockchainInput{ - Name: &nockchain.NockchainName{ - First: crypto.Tip5HashToBase58(firstNames[i]), - Last: crypto.Tip5HashToBase58(lastNames[i]), - }, + Name: nnames[i], Note: notes[i], Spend: &spend, } @@ -478,7 +518,7 @@ func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxReque TxId: "", Inputs: inputs, TimelockRange: timelockRange, - TotalFees: req.Fee * uint64(len(inputs)), + TotalFees: req.Fee, } txId, err := ComputeTxId(inputs, timelockRange, req.Fee) if err != nil { diff --git a/wallet/service_test.go b/wallet/service_test.go index 085b33f..9abffcf 100644 --- a/wallet/service_test.go +++ b/wallet/service_test.go @@ -2,6 +2,7 @@ package wallet_test import ( "context" + "fmt" "math/big" "slices" "testing" @@ -424,3 +425,42 @@ func TestFullFlow(t *testing.T) { Accepted: true, }) } + +// This test should be run with timeout 300s +func TestCreateTx(t *testing.T) { + seed1 := "pledge vessel toilet sunny hockey skirt spend wire disorder attitude crumble lecture problem bundle bone rather address over suit ancient primary gospel silent repair" + masterKey1, err := crypto.MasterKeyFromSeed(seed1) + assert.NoError(t, err) + + seed2 := "brass vacuum stairs hurt brisk govern describe enforce fly exact rescue capable belt flavor lottery sauce easy frame orange legal injury border obey novel" + masterKey2, err := crypto.MasterKeyFromSeed(seed2) + assert.NoError(t, err) + + nc, err := wallet.NewNockchainClient("nockchain-api.zorp.io:443") + assert.NoError(t, err) + handler := wallet.NewGprcHandler(*nc) + + inputs := "[CxiTK4HjqPRebUkoy6rH89ZcNGuH4goHkmKgmgCJxZEFS2C3qrDoh4y 91z5muQKgHZDcChdnCSTEtvuN8dbbXp5wzNs5xrCkce2YvSX1q6fu3d],[CxiTK4HjqPRebUkoy6rH89ZcNGuH4goHkmKgmgCJxZEFS2C3qrDoh4y 3a9VWigUM1TuS79yM4dAqkiUg6WJMUPGwVKJpQUHCLJcFZsCWM2q6pF],[CxiTK4HjqPRebUkoy6rH89ZcNGuH4goHkmKgmgCJxZEFS2C3qrDoh4y 9RkPvHMtuYtjV2qvB86u7zcnr26SdVcKzFnHgfhUSEG1W3FgKgLpbBm]" + recipients := fmt.Sprintf("%s,%s,%s", base58.Encode(masterKey2.PublicKey), base58.Encode(masterKey2.PublicKey), base58.Encode(masterKey2.PublicKey)) + gifts := "100,100,100" + fee := 100 + + req := &nockchain.CreateTxRequest{ + Names: inputs, + Recipients: recipients, + Gifts: gifts, + Fee: uint64(fee), + IsMasterKey: true, + Key: base58.Encode(masterKey1.PrivateKey), + ChainCode: base58.Encode(masterKey1.ChainCode), + Index: 0, + Hardened: false, + TimelockIntent: nil, + } + + res, err := handler.CreateTx(context.Background(), req) + assert.NoError(t, err) + + // the result is taken from create-tx scripts + assert.Equal(t, res.RawTx.TxId, "8gjTa6H1MrKXPWgNJCF9fsYE7PUfqhYzxetUoTftz7zyHCv24ZYHDM3") +} diff --git a/wallet/ztree.go b/wallet/ztree.go new file mode 100644 index 0000000..ce8d3d6 --- /dev/null +++ b/wallet/ztree.go @@ -0,0 +1,140 @@ +package wallet + +import ( + "slices" + + "github.com/phamminh0811/private-grpc/crypto" +) + +type ZNode struct { + Key interface{} + Value interface{} + + Left *ZNode + Right *ZNode +} + +type ZTree struct { + Root *ZNode + HashKeyFunc func(interface{}) [5]uint64 + HashValueFunc func(interface{}) ([5]uint64, error) +} + +func NewZTree(hashKeyFunc func(interface{}) [5]uint64, hashValueFunc func(interface{}) ([5]uint64, error)) *ZTree { + return &ZTree{ + Root: nil, + HashKeyFunc: hashKeyFunc, + HashValueFunc: hashValueFunc, + } +} + +func (z *ZTree) Insert(key, value interface{}) { + z.Root = z.Root.InsertNode(z.HashKeyFunc, key, value) +} + +func (z *ZTree) Hash() ([5]uint64, error) { + return z.Root.HashNode(z.HashValueFunc) +} + +func (z *ZTree) KeyLeftMost() interface{} { + node := z.Root + for node.Left != nil { + node = node.Left + } + return node.Key +} +func (node *ZNode) InsertNode(hashFunc func(interface{}) [5]uint64, key, value interface{}) *ZNode { + if node == nil { + node = &ZNode{ + Key: key, + Value: value, + Left: nil, + Right: nil, + } + return node + } + + keyHash := hashFunc(key) + keyDoubleHash := crypto.Tip5RehashTenCell(keyHash, keyHash) + nodeKeyHash := hashFunc(node.Key) + nodeKeyDoubleHash := crypto.Tip5RehashTenCell(nodeKeyHash, nodeKeyHash) + + if slices.Compare(keyHash[:], nodeKeyHash[:]) == -1 { + // key < node key + if slices.Compare(keyDoubleHash[:], nodeKeyDoubleHash[:]) == -1 { + // reinsert in left + node.Left = node.Left.InsertNode(hashFunc, key, value) + } else { + // new key + // / \ + // ~ old key + // / \ + // ... ... + nodeKey := node.Key + nodeValue := node.Value + leftNode := node.Left + rightNode := node.Right + node = &ZNode{ + Key: key, + Value: value, + Right: nil, + Left: &ZNode{ + Key: nodeKey, + Value: nodeValue, + Left: leftNode, + Right: rightNode, + }, + } + } + } else { + // key > node key + if slices.Compare(keyDoubleHash[:], nodeKeyDoubleHash[:]) == -1 { + // reinsert in right + node.Right = node.Right.InsertNode(hashFunc, key, value) + } else { + // new key + // / \ + // old key ~ + // / \ + // ... ... + nodeKey := node.Key + nodeValue := node.Value + leftNode := node.Left + rightNode := node.Right + node = &ZNode{ + Key: key, + Value: value, + Right: &ZNode{ + Key: nodeKey, + Value: nodeValue, + Left: leftNode, + Right: rightNode, + }, + Left: nil, + } + } + } + return node +} + +func (node *ZNode) HashNode(hashFunc func(interface{}) ([5]uint64, error)) ([5]uint64, error) { + if node == nil { + return crypto.Tip5Zero, nil + } + + leftHash, err := node.Left.HashNode(hashFunc) + if err != nil { + return [5]uint64{}, err + } + rightHash, err := node.Right.HashNode(hashFunc) + if err != nil { + return [5]uint64{}, err + } + + hashLeftRight := crypto.Tip5RehashTenCell(leftHash, rightHash) + valHash, err := hashFunc(node.Value) + if err != nil { + return [5]uint64{}, err + } + return crypto.Tip5RehashTenCell(valHash, hashLeftRight), nil +}