fix: send tx with 2 notes

This commit is contained in:
Anh Minh 2025-10-16 12:31:21 +07:00
parent 552a9e9d9b
commit e34c2738d0
4 changed files with 262 additions and 25 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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")
}

140
wallet/ztree.go Normal file
View File

@ -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
}