diff --git a/apps/mixin/account.go b/apps/mixin/account.go new file mode 100644 index 00000000..3ab4e215 --- /dev/null +++ b/apps/mixin/account.go @@ -0,0 +1,83 @@ +package mixin + +import ( + "encoding/hex" + "fmt" + + "github.com/MixinNetwork/mixin/common" + "github.com/MixinNetwork/mixin/crypto" + "github.com/MixinNetwork/multi-party-sig/pkg/math/curve" +) + +func VerifyPublicKey(pub string) error { + key, err := crypto.KeyFromString(pub) + if err != nil { + return err + } + if !key.CheckKey() { + return fmt.Errorf("invalid mixin public key %s", pub) + } + return nil +} + +func VerifySignature(public string, msg, sig []byte) error { + var msig crypto.Signature + if len(sig) != len(msig) { + return fmt.Errorf("invalid mixin signature %x", sig) + } + copy(msig[:], sig) + key, err := crypto.KeyFromString(public) + if err != nil { + return err + } + if key.Verify(msg, msig) { + return nil + } + return fmt.Errorf("mixin.VerifySignature(%s, %x, %x)", public, msg, sig) +} + +func DeriveKey(signer string, mask []byte) string { + group := curve.Edwards25519{} + r := group.NewScalar() + err := r.UnmarshalBinary(mask) + if err != nil { + panic(err) + } + key, err := crypto.KeyFromString(signer) + if err != nil || !key.CheckKey() { + panic(signer) + } + P := group.NewPoint() + err = P.UnmarshalBinary(key[:]) + if err != nil { + panic(err) + } + P = r.ActOnBase().Add(P) + b, err := P.MarshalBinary() + if err != nil { + panic(err) + } + return hex.EncodeToString(b) +} + +func ParseAddress(s string) (*common.Address, error) { + addr, err := common.NewAddressFromString(s) + return &addr, err +} + +func BuildAddress(holder, signer, observer string) *common.Address { + for _, k := range []string{holder, signer, observer} { + err := VerifyPublicKey(k) + if err != nil { + panic(k) + } + } + seed := crypto.NewHash([]byte(holder + signer + observer)) + view := crypto.NewKeyFromSeed(append(seed[:], seed[:]...)) + publicSpend, _ := crypto.KeyFromString(signer) + return &common.Address{ + PublicSpendKey: publicSpend, + PublicViewKey: view.Public(), + PrivateViewKey: view, + } +} diff --git a/apps/mixin/common.go b/apps/mixin/common.go new file mode 100644 index 00000000..05653087 --- /dev/null +++ b/apps/mixin/common.go @@ -0,0 +1,22 @@ +package mixin + +import ( + "bytes" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +const ( + ChainMixinKernel = 3 + ValuePrecision = 8 + ValueDust = 10000 +) + +func HashMessageForSignature(msg string) []byte { + var buf bytes.Buffer + prefix := "Mixin Signed Message:\n" + _ = wire.WriteVarString(&buf, 0, prefix) + _ = wire.WriteVarString(&buf, 0, msg) + return chainhash.DoubleHashB(buf.Bytes()) +} diff --git a/apps/mixin/transaction.go b/apps/mixin/transaction.go new file mode 100644 index 00000000..cb257116 --- /dev/null +++ b/apps/mixin/transaction.go @@ -0,0 +1,99 @@ +package mixin + +import ( + "fmt" + + "github.com/MixinNetwork/mixin/common" + "github.com/MixinNetwork/mixin/crypto" + "github.com/MixinNetwork/safe/apps/bitcoin" + "github.com/gofrs/uuid/v5" + "github.com/shopspring/decimal" +) + +type Input struct { + TransactionHash string + Index uint32 + Amount decimal.Decimal + Asset crypto.Hash + Mask crypto.Key +} + +type Output struct { + Address *common.Address + Amount decimal.Decimal +} + +func ParseTransactionDepositOutput(holder, signer, observer string, mtx *common.VersionedTransaction, index int) (*Input, string) { + if len(mtx.Outputs) < index+1 { + return nil, "" + } + out := mtx.Outputs[index] + if out.Type != common.OutputTypeScript { + return nil, "" + } + if out.Script.String() != "fffe01" { + return nil, "" + } + if len(out.Keys) != 1 { + return nil, "" + } + addr := BuildAddress(holder, signer, observer) + pub := crypto.ViewGhostOutputKey(out.Keys[0], &addr.PrivateViewKey, &out.Mask, uint64(index)) + if pub.String() != addr.PublicSpendKey.String() { + return nil, "" + } + input := &Input{ + TransactionHash: mtx.PayloadHash().String(), + Index: uint32(index), + Amount: decimal.RequireFromString(out.Amount.String()), + Asset: mtx.Asset, + Mask: out.Mask, + } + return input, addr.String() +} + +func BuildPartiallySignedTransaction(mainInputs []*Input, outputs []*Output, rid string, holder, signer, observer string) (*common.VersionedTransaction, error) { + var input, output decimal.Decimal + tx := common.NewTransactionV4(mainInputs[0].Asset) + for _, in := range mainInputs { + if in.Asset != tx.Asset { + panic(in.Asset.String()) + } + input = input.Add(in.Amount) + hash, err := crypto.HashFromString(in.TransactionHash) + if err != nil { + panic(in.TransactionHash) + } + tx.AddInput(hash, int(in.Index)) + } + + si := crypto.NewHash([]byte("SEED:" + holder + signer + observer + rid)) + si = crypto.NewHash(append(tx.AsVersioned().PayloadMarshal(), si[:]...)) + for i, out := range outputs { + output = output.Add(out.Amount) + script := common.NewThresholdScript(1) + amount := common.NewIntegerFromString(out.Amount.String()) + os := fmt.Sprintf("%x:%s:%s:%d", si[:], out.Address.String(), out.Amount.String(), i) + seed := crypto.NewHash([]byte(os)) + tx.AddScriptOutput([]*common.Address{out.Address}, script, amount, append(seed[:], seed[:]...)) + } + + if input.Cmp(output) < 0 { + return nil, bitcoin.BuildInsufficientInputError("main", input.String(), output.String()) + } + change := input.Sub(output) + addr := BuildAddress(holder, signer, observer) + if change.IsPositive() { + script := common.NewThresholdScript(1) + amount := common.NewIntegerFromString(change.String()) + seed := crypto.NewHash([]byte(fmt.Sprintf("%x:%d", si[:], len(outputs)))) + tx.AddScriptOutput([]*common.Address{addr}, script, amount, append(seed[:], seed[:]...)) + } + + tx.Extra = uuid.Must(uuid.FromString(rid)).Bytes() + return tx.AsVersioned(), nil +} + +func ParsePartiallySignedTransaction(b []byte) (*common.VersionedTransaction, error) { + return common.UnmarshalVersionedTransaction(b) +} diff --git a/common/mixin.go b/common/mixin.go index 3a525217..b1d0f8f4 100644 --- a/common/mixin.go +++ b/common/mixin.go @@ -21,6 +21,8 @@ import ( "github.com/shopspring/decimal" ) +type VersionedTransaction = common.VersionedTransaction + // TODO the output should include the snapshot signature, then it can just be // verified against the active kernel nodes public key func VerifyKernelTransaction(rpc string, out *mtg.Output, timeout time.Duration) error { @@ -136,6 +138,26 @@ func ReadKernelTransaction(rpc string, tx crypto.Hash) (*common.VersionedTransac return common.UnmarshalVersionedTransaction(hex) } +func ReadKernelSnapshot(rpc string, id crypto.Hash) (*common.SnapshotWithTopologicalOrder, error) { + raw, err := callMixinRPC(rpc, "getsnapshot", []any{id.String()}) + if err != nil { + return nil, err + } + var snap map[string]any + err = json.Unmarshal(raw, &snap) + if err != nil { + return nil, err + } + if snap["hex"] == nil { + return nil, fmt.Errorf("snap %s not found in kernel", id) + } + hex, err := hex.DecodeString(snap["hex"].(string)) + if err != nil { + return nil, err + } + return common.UnmarshalVersionedSnapshot(hex) +} + func callMixinRPC(node, method string, params []any) ([]byte, error) { client := &http.Client{Timeout: 20 * time.Second} diff --git a/common/request.go b/common/request.go index 74abf175..91fbf073 100644 --- a/common/request.go +++ b/common/request.go @@ -9,6 +9,7 @@ import ( "github.com/MixinNetwork/mixin/crypto" "github.com/MixinNetwork/safe/apps/bitcoin" "github.com/MixinNetwork/safe/apps/ethereum" + "github.com/MixinNetwork/safe/apps/mixin" "github.com/MixinNetwork/trusted-group/mtg" "github.com/gofrs/uuid/v5" "github.com/shopspring/decimal" @@ -45,11 +46,12 @@ const ( ActionBitcoinSafeCloseAccount = 115 // For Mixin Kernel mainnet - ActionMixinSafeProposeAccount = 120 - ActionMixinSafeApproveAccount = 121 - ActionMixinSafeProposeTransaction = 122 - ActionMixinSafeApproveTransaction = 123 - ActionMixinSafeRevokeTransaction = 124 + ActionMixinKernelSafeProposeAccount = 120 + ActionMixinKernelSafeApproveAccount = 121 + ActionMixinKernelSafeProposeTransaction = 122 + ActionMixinKernelSafeApproveTransaction = 123 + ActionMixinKernelSafeRevokeTransaction = 124 + ActionMixinKernelSafeCloseAccount = 125 // For all Ethereum like chains ActionEthereumSafeProposeAccount = 130 @@ -122,6 +124,7 @@ func DecodeRequest(out *mtg.Output, b []byte, role uint8) (*Request, error) { func (req *Request) ParseMixinRecipient(extra []byte) (*AccountProposal, error) { switch req.Action { case ActionBitcoinSafeProposeAccount: + case ActionMixinKernelSafeProposeAccount: case ActionEthereumSafeProposeAccount: default: panic(req.Action) @@ -202,6 +205,8 @@ func (r *Request) VerifyFormat() error { switch r.Curve { case CurveSecp256k1ECDSABitcoin, CurveSecp256k1ECDSALitecoin: return bitcoin.VerifyHolderKey(r.Holder) + case CurveEdwards25519Mixin: + return mixin.VerifyPublicKey(r.Holder) case CurveSecp256k1ECDSAEthereum, CurveSecp256k1ECDSAMVM, CurveSecp256k1ECDSAPolygon: return ethereum.VerifyHolderKey(r.Holder) default: diff --git a/go.mod b/go.mod index 3cb35c9a..27f5b4df 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/MixinNetwork/safe go 1.21 require ( - github.com/MixinNetwork/bot-api-go-client v1.8.6 + github.com/MixinNetwork/bot-api-go-client v1.8.7 github.com/MixinNetwork/go-number v0.1.1 github.com/MixinNetwork/mixin v0.16.10 github.com/MixinNetwork/multi-party-sig v0.3.2 diff --git a/go.sum b/go.sum index 835b787d..a1c0062e 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/MixinNetwork/bot-api-go-client v1.8.6 h1:QzA+yXJuDXpOX+1xvlw76/wtfhQDAa+TA/YXB3XaWK4= -github.com/MixinNetwork/bot-api-go-client v1.8.6/go.mod h1:Phc+juNRPDadsTUfLOeIbYHX6AiePofe8YH3gIxARZ8= +github.com/MixinNetwork/bot-api-go-client v1.8.7 h1:+BMgP0hbZFV+Z77DZ9AYWCXBYYgKuAWf2GrtfkefKsY= +github.com/MixinNetwork/bot-api-go-client v1.8.7/go.mod h1:RJdkxQIdNO0X17SKAoJ8WCSP4QVeDblDulkauSk9ctw= github.com/MixinNetwork/go-number v0.1.1 h1:Ui/xi0WGiBWI6cPrZaffB6q8lP7m2Zw0CXgOqLXb/3c= github.com/MixinNetwork/go-number v0.1.1/go.mod h1:4kaXQW9NOjjO3uZ5ehRVn3m+G+5ENGEKgiwfxea3zGQ= github.com/MixinNetwork/mixin v0.16.10 h1:cTZ7Ic/BFe3HNX7X1raqxsapj/f8117iBCalIC/lXmg= diff --git a/keeper/bitcoin.go b/keeper/bitcoin.go index 88970aa5..41e20a0a 100644 --- a/keeper/bitcoin.go +++ b/keeper/bitcoin.go @@ -518,6 +518,7 @@ func (node *Node) processBitcoinSafeProposeTransaction(ctx context.Context, req if err != nil { return node.store.FailRequest(ctx, req.Id) } + var total decimal.Decimal for _, rp := range recipients { script, err := bitcoin.ParseAddress(rp[0], safe.Chain) logger.Printf("bitcoin.ParseAddress(%s, %d) => %x %v", string(extra), safe.Chain, script, err) @@ -531,11 +532,15 @@ func (node *Node) processBitcoinSafeProposeTransaction(ctx context.Context, req if amt.Cmp(plan.TransactionMinimum) < 0 { return node.store.FailRequest(ctx, req.Id) } + total = total.Add(amt) outputs = append(outputs, &bitcoin.Output{ Address: rp[0], Satoshi: bitcoin.ParseSatoshi(amt.String()), }) } + if !total.Equal(req.Amount) { + return node.store.FailRequest(ctx, req.Id) + } } else { script, err := bitcoin.ParseAddress(string(extra[16:]), safe.Chain) logger.Printf("bitcoin.ParseAddress(%s, %d) => %x %v", string(extra), safe.Chain, script, err) diff --git a/keeper/common.go b/keeper/common.go index 64416a7a..b6dc4193 100644 --- a/keeper/common.go +++ b/keeper/common.go @@ -10,6 +10,7 @@ import ( "github.com/MixinNetwork/mixin/logger" "github.com/MixinNetwork/safe/apps/bitcoin" "github.com/MixinNetwork/safe/apps/ethereum" + "github.com/MixinNetwork/safe/apps/mixin" "github.com/MixinNetwork/safe/common" "github.com/MixinNetwork/safe/common/abi" "github.com/MixinNetwork/safe/keeper/store" @@ -17,17 +18,19 @@ import ( ) const ( - SafeChainBitcoin = bitcoin.ChainBitcoin - SafeChainLitecoin = bitcoin.ChainLitecoin - SafeChainEthereum = ethereum.ChainEthereum - SafeChainMVM = ethereum.ChainMVM - SafeChainPolygon = ethereum.ChainPolygon + SafeChainMixinKernel = mixin.ChainMixinKernel + SafeChainBitcoin = bitcoin.ChainBitcoin + SafeChainLitecoin = bitcoin.ChainLitecoin + SafeChainEthereum = ethereum.ChainEthereum + SafeChainMVM = ethereum.ChainMVM + SafeChainPolygon = ethereum.ChainPolygon - SafeBitcoinChainId = "c6d0c728-2624-429b-8e0d-d9d19b6592fa" - SafeEthereumChainId = "43d61dcd-e413-450d-80b8-101d5e903357" - SafeMVMChainId = "a0ffd769-5850-4b48-9651-d2ae44a3e64d" - SafeLitecoinChainId = "76c802a2-7c88-447f-a93e-c29c9e5dd9c8" - SafePolygonChainId = "b7938396-3f94-4e0a-9179-d3440718156f" + SafeMixinKernelAssetId = "c94ac88f-4671-3976-b60a-09064f1811e8" + SafeBitcoinChainId = "c6d0c728-2624-429b-8e0d-d9d19b6592fa" + SafeEthereumChainId = "43d61dcd-e413-450d-80b8-101d5e903357" + SafeMVMChainId = "a0ffd769-5850-4b48-9651-d2ae44a3e64d" + SafeLitecoinChainId = "76c802a2-7c88-447f-a93e-c29c9e5dd9c8" + SafePolygonChainId = "b7938396-3f94-4e0a-9179-d3440718156f" SafeSignatureTimeout = 10 * time.Minute SafeKeyBackupMaturity = 24 * time.Hour @@ -37,6 +40,10 @@ const ( SafeStateClosed = common.RequestStateFailed ) +func mixinDefaultDerivationPath() []byte { + return []byte{0, 0, 0, 0} +} + func bitcoinDefaultDerivationPath() []byte { return []byte{2, 0, 0, 0} } @@ -90,7 +97,7 @@ func (node *Node) refundAndFailRequest(ctx context.Context, req *common.Request, func (node *Node) bondMaxSupply(ctx context.Context, chain byte, assetId string) decimal.Decimal { switch assetId { - case SafeBitcoinChainId, SafeLitecoinChainId, SafeEthereumChainId, SafeMVMChainId, SafePolygonChainId: + case SafeBitcoinChainId, SafeLitecoinChainId, SafeEthereumChainId, SafeMVMChainId, SafePolygonChainId, SafeMixinKernelAssetId: return decimal.RequireFromString("115792089237316195423570985008687907853269984665640564039457.58400791") default: return decimal.RequireFromString("115792089237316195423570985008687907853269984665640564039457.58400791") diff --git a/keeper/deposit.go b/keeper/deposit.go index 71825355..8933af75 100644 --- a/keeper/deposit.go +++ b/keeper/deposit.go @@ -42,12 +42,22 @@ func parseDepositExtra(req *common.Request) (*Deposit, error) { extra = extra[17:] switch deposit.Chain { case SafeChainBitcoin, SafeChainLitecoin: + if deposit.Chain != SafeCurveChain(req.Curve) { + panic(req.Id) + } deposit.Hash = hex.EncodeToString(extra[0:32]) deposit.Index = binary.BigEndian.Uint64(extra[32:40]) deposit.Amount = new(big.Int).SetBytes(extra[40:]) if !deposit.Amount.IsInt64() { return nil, fmt.Errorf("invalid deposit amount %s", deposit.Amount.String()) } + case SafeChainMixinKernel: + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Id) + } + deposit.Hash = hex.EncodeToString(extra[0:32]) + deposit.Index = binary.BigEndian.Uint64(extra[32:40]) + deposit.Amount = new(big.Int).SetBytes(extra[40:]) case SafeChainEthereum, SafeChainMVM, SafeChainPolygon: deposit.Hash = "0x" + hex.EncodeToString(extra[0:32]) deposit.AssetAddress = gc.BytesToAddress(extra[32:52]).Hex() @@ -103,7 +113,7 @@ func (node *Node) CreateHolderDeposit(ctx context.Context, req *common.Request) if err != nil { return fmt.Errorf("node.fetchAssetMeta(%s) => %v", deposit.Asset, err) } - if asset.Chain != safe.Chain { + if asset.Chain != safe.Chain && safe.Chain != SafeChainMixinKernel { panic(asset.AssetId) } @@ -118,6 +128,8 @@ func (node *Node) CreateHolderDeposit(ctx context.Context, req *common.Request) switch deposit.Chain { case SafeChainBitcoin, SafeChainLitecoin: return node.doBitcoinHolderDeposit(ctx, req, deposit, safe, bond.AssetId, asset, plan.TransactionMinimum) + case SafeChainMixinKernel: + return node.doMixinKernelHolderDeposit(ctx, req, deposit, safe, bond.AssetId, plan.TransactionMinimum) case SafeChainEthereum, SafeChainMVM, SafeChainPolygon: return node.doEthereumHolderDeposit(ctx, req, deposit, safe, bond.AssetId, asset, plan.TransactionMinimum) default: @@ -126,6 +138,7 @@ func (node *Node) CreateHolderDeposit(ctx context.Context, req *common.Request) } // FIXME Keeper should deny new deposits when too many unspent outputs +// for mixin kernel this value is 256 func (node *Node) doBitcoinHolderDeposit(ctx context.Context, req *common.Request, deposit *Deposit, safe *store.Safe, bondId string, asset *store.Asset, minimum decimal.Decimal) error { if asset.Decimals != bitcoin.ValuePrecision { panic(asset.Decimals) diff --git a/keeper/group.go b/keeper/group.go index 6c4fd119..b13afb37 100644 --- a/keeper/group.go +++ b/keeper/group.go @@ -9,6 +9,7 @@ import ( "github.com/MixinNetwork/mixin/logger" "github.com/MixinNetwork/safe/apps/bitcoin" "github.com/MixinNetwork/safe/apps/ethereum" + "github.com/MixinNetwork/safe/apps/mixin" "github.com/MixinNetwork/safe/common" "github.com/MixinNetwork/safe/common/abi" "github.com/MixinNetwork/trusted-group/mtg" @@ -42,6 +43,12 @@ func (node *Node) ProcessOutput(ctx context.Context, out *mtg.Output) bool { case common.ActionBitcoinSafeApproveTransaction: case common.ActionBitcoinSafeRevokeTransaction: case common.ActionBitcoinSafeCloseAccount: + case common.ActionMixinKernelSafeProposeAccount: + case common.ActionMixinKernelSafeApproveAccount: + case common.ActionMixinKernelSafeProposeTransaction: + case common.ActionMixinKernelSafeApproveTransaction: + case common.ActionMixinKernelSafeRevokeTransaction: + case common.ActionMixinKernelSafeCloseAccount: case common.ActionEthereumSafeProposeAccount: case common.ActionEthereumSafeApproveAccount: case common.ActionEthereumSafeProposeTransaction: @@ -87,15 +94,15 @@ func (node *Node) getActionRole(act byte) byte { return common.RequestRoleObserver case common.ActionObserverSetOperationParams: return common.RequestRoleObserver - case common.ActionBitcoinSafeProposeAccount, common.ActionEthereumSafeProposeAccount: + case common.ActionBitcoinSafeProposeAccount, common.ActionEthereumSafeProposeAccount, common.ActionMixinKernelSafeProposeAccount: return common.RequestRoleHolder - case common.ActionBitcoinSafeApproveAccount, common.ActionEthereumSafeApproveAccount: + case common.ActionBitcoinSafeApproveAccount, common.ActionEthereumSafeApproveAccount, common.ActionMixinKernelSafeApproveAccount: return common.RequestRoleObserver - case common.ActionBitcoinSafeProposeTransaction, common.ActionEthereumSafeProposeTransaction: + case common.ActionBitcoinSafeProposeTransaction, common.ActionEthereumSafeProposeTransaction, common.ActionMixinKernelSafeProposeTransaction: return common.RequestRoleHolder - case common.ActionBitcoinSafeApproveTransaction, common.ActionEthereumSafeApproveTransaction: + case common.ActionBitcoinSafeApproveTransaction, common.ActionEthereumSafeApproveTransaction, common.ActionMixinKernelSafeApproveTransaction: return common.RequestRoleObserver - case common.ActionBitcoinSafeRevokeTransaction, common.ActionEthereumSafeRevokeTransaction: + case common.ActionBitcoinSafeRevokeTransaction, common.ActionEthereumSafeRevokeTransaction, common.ActionMixinKernelSafeRevokeTransaction: return common.RequestRoleObserver case common.ActionBitcoinSafeCloseAccount, common.ActionEthereumSafeCloseAccount: return common.RequestRoleObserver @@ -234,6 +241,18 @@ func (node *Node) processRequest(ctx context.Context, req *common.Request) error return node.processSafeRevokeTransaction(ctx, req) case common.ActionBitcoinSafeCloseAccount: return node.processBitcoinSafeCloseAccount(ctx, req) + case common.ActionMixinKernelSafeProposeAccount: + return node.processMixinKernelSafeProposeAccount(ctx, req) + case common.ActionMixinKernelSafeApproveAccount: + return node.processMixinKernelSafeApproveAccount(ctx, req) + case common.ActionMixinKernelSafeProposeTransaction: + return node.processMixinKernelSafeProposeTransaction(ctx, req) + case common.ActionMixinKernelSafeApproveTransaction: + return node.processMixinKernelSafeApproveTransaction(ctx, req) + case common.ActionMixinKernelSafeRevokeTransaction: + return node.processMixinKernelSafeRevokeTransaction(ctx, req) + case common.ActionMixinKernelSafeCloseAccount: + return node.processMixinKernelSafeCloseAccount(ctx, req) case common.ActionEthereumSafeProposeAccount: return node.processEthereumSafeProposeAccount(ctx, req) case common.ActionEthereumSafeApproveAccount: @@ -292,6 +311,9 @@ func (node *Node) processKeyAdd(ctx context.Context, req *common.Request) error if err != nil { return node.store.FailRequest(ctx, req.Id) } + case common.CurveEdwards25519Mixin: + err = mixin.VerifyPublicKey(req.Holder) + logger.Printf("mixin.VerifyPublicKey(%s, %x) => %v", req.Holder, chainCode, err) case common.CurveSecp256k1ECDSAEthereum, common.CurveSecp256k1ECDSAMVM, common.CurveSecp256k1ECDSAPolygon: err = ethereum.VerifyHolderKey(req.Holder) logger.Printf("ethereum.VerifyHolderKey(%s, %x) => %v", req.Holder, chainCode, err) @@ -332,6 +354,8 @@ func (node *Node) processSignerSignatureResponse(ctx context.Context, req *commo return node.processBitcoinSafeSignatureResponse(ctx, req, safe, tx, old) case SafeChainEthereum, SafeChainMVM, SafeChainPolygon: return node.processEthereumSafeSignatureResponse(ctx, req, safe, tx, old) + case SafeChainMixinKernel: + return node.processMixinKernelSafeSignatureResponse(ctx, req) default: panic(safe.Chain) } diff --git a/keeper/keeper_test.go b/keeper/keeper_test.go index 9e90f11b..71cb7f43 100644 --- a/keeper/keeper_test.go +++ b/keeper/keeper_test.go @@ -843,6 +843,10 @@ func testReadObserverResponse(ctx context.Context, require *require.Assertions, params, _ := node.store.ReadLatestOperationParams(ctx, SafeChainBitcoin, time.Now()) require.Equal(params.OperationPriceAsset, om["asset_id"]) require.Equal(params.OperationPriceAmount.String(), om["amount"]) + case common.ActionMixinKernelSafeApproveAccount: + params, _ := node.store.ReadLatestOperationParams(ctx, SafeChainMixinKernel, time.Now()) + require.Equal(params.OperationPriceAsset, om["asset_id"]) + require.Equal(params.OperationPriceAmount.String(), om["amount"]) case common.ActionEthereumSafeApproveAccount: params, _ := node.store.ReadLatestOperationParams(ctx, SafeChainPolygon, time.Now()) require.Equal(params.OperationPriceAsset, om["asset_id"]) @@ -868,6 +872,8 @@ func testBuildHolderRequest(node *Node, id, public string, action byte, assetId crv := byte(common.CurveSecp256k1ECDSABitcoin) switch action { case common.ActionBitcoinSafeProposeAccount, common.ActionBitcoinSafeProposeTransaction: + case common.ActionMixinKernelSafeProposeAccount, common.ActionMixinKernelSafeProposeTransaction: + crv = common.CurveEdwards25519Mixin case common.ActionEthereumSafeProposeAccount, common.ActionEthereumSafeProposeTransaction: crv = common.CurveSecp256k1ECDSAPolygon } @@ -917,6 +923,8 @@ func testBuildSignerOutput(node *Node, id, public string, action byte, extra []b path := bitcoinDefaultDerivationPath() switch crv { case common.CurveSecp256k1ECDSABitcoin: + case common.CurveEdwards25519Mixin: + path = mixinDefaultDerivationPath() case common.CurveSecp256k1ECDSAEthereum, common.CurveSecp256k1ECDSAPolygon: path = ethereumDefaultDerivationPath() default: diff --git a/keeper/mixin.go b/keeper/mixin.go index 5fc6ac2d..765a7413 100644 --- a/keeper/mixin.go +++ b/keeper/mixin.go @@ -1,6 +1,730 @@ package keeper +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/MixinNetwork/mixin/crypto" + "github.com/MixinNetwork/mixin/logger" + "github.com/MixinNetwork/safe/apps/bitcoin" + "github.com/MixinNetwork/safe/apps/mixin" + "github.com/MixinNetwork/safe/common" + "github.com/MixinNetwork/safe/common/abi" + "github.com/MixinNetwork/safe/keeper/store" + "github.com/gofrs/uuid/v5" + "github.com/shopspring/decimal" +) + +// We will always only allow XIN deposit for mixin kernel, because this is the only use case. +// But all code should imply more assets may come in the future. We make this decision, because +// the native chain safe is feasible for all other assets. +func (node *Node) doMixinKernelHolderDeposit(ctx context.Context, req *common.Request, deposit *Deposit, safe *store.Safe, bondId string, minimum decimal.Decimal) error { + if deposit.Asset != SafeMixinKernelAssetId { + return node.store.FailRequest(ctx, req.Id) + } + old, _, err := node.store.ReadMixinKernelUTXO(ctx, deposit.Hash, int(deposit.Index)) + logger.Printf("store.ReadMixinKernelUTXO(%s, %d) => %v %v", deposit.Hash, deposit.Index, old, err) + if err != nil { + return fmt.Errorf("store.ReadMixinKernelUTXO(%s, %d) => %v", deposit.Hash, deposit.Index, err) + } else if old != nil { + return node.store.FailRequest(ctx, req.Id) + } + + dh, err := crypto.HashFromString(deposit.Hash) + if err != nil { + panic(err) + } + mtx, err := common.ReadKernelTransaction(node.conf.MixinRPC, dh) + if err != nil { + return fmt.Errorf("common.ReadKernelTransaction(%s) => %v", deposit.Hash, err) + } + + amount := decimal.NewFromBigInt(deposit.Amount, -mixin.ValuePrecision) + change, err := node.checkMixinKernelChange(ctx, deposit, mtx) + logger.Printf("node.checkMixinKernelChange(%v, %v) => %t %v", deposit, mtx, change, err) + if err != nil { + return fmt.Errorf("node.checkMixinKernelChange(%v) => %v", deposit, err) + } + if amount.Cmp(minimum) < 0 && !change { + return node.store.FailRequest(ctx, req.Id) + } + if amount.Cmp(decimal.New(mixin.ValueDust, -mixin.ValuePrecision)) < 0 { + panic(deposit.Hash) + } + + output, err := node.verifyMixinKernelTransaction(ctx, req, deposit, mtx, safe) + logger.Printf("node.verifyMixinKernelTransaction(%v) => %v %v", req, output, err) + if err != nil { + return fmt.Errorf("node.verifyMixinKernelTransaction(%s) => %v", deposit.Hash, err) + } + if output == nil { + return node.store.FailRequest(ctx, req.Id) + } + + if !change { + err = node.buildTransaction(ctx, bondId, safe.Receivers, int(safe.Threshold), amount.String(), nil, req.Id) + if err != nil { + return fmt.Errorf("node.buildTransaction(%v) => %v", req, err) + } + } + + return node.store.WriteMixinKernelOutputFromRequest(ctx, safe.Address, deposit.Asset, output, req) +} + // holder key just for safe verification, not for kernel -// observer holds the view key, i.e. the accountant key -// no recovery key needed, and signer holds spend key -// the risk here is the accountant key loss +// the kernel view key is derived by hash of holder, signer and observer +// then keeper and observer must never disclose the observer public key +// thus the kernel view key remains anonymous from public +func (node *Node) processMixinKernelSafeProposeAccount(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleHolder { + panic(req.Role) + } + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Curve) + } + chain := byte(SafeChainMixinKernel) + rce, err := hex.DecodeString(req.Extra) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + ver, _ := common.ReadKernelTransaction(node.conf.MixinRPC, req.MixinHash) + if len(rce) == 32 && len(ver.References) == 1 && ver.References[0].String() == req.Extra { + stx, _ := common.ReadKernelTransaction(node.conf.MixinRPC, ver.References[0]) + rce = common.DecodeMixinObjectExtra(stx.Extra) + } + arp, err := req.ParseMixinRecipient(rce) + logger.Printf("req.ParseMixinRecipient(%v) => %v %v", req, arp, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + + plan, err := node.store.ReadLatestOperationParams(ctx, chain, req.CreatedAt) + logger.Printf("store.ReadLatestOperationParams(%d) => %v %v", chain, plan, err) + if err != nil { + return fmt.Errorf("node.ReadLatestOperationParams(%d) => %v", chain, err) + } else if plan == nil || !plan.OperationPriceAmount.IsPositive() { + return node.refundAndFailRequest(ctx, req, arp.Receivers, int(arp.Threshold)) + } + if req.AssetId != plan.OperationPriceAsset { + return node.store.FailRequest(ctx, req.Id) + } + if req.Amount.Cmp(plan.OperationPriceAmount) < 0 { + return node.store.FailRequest(ctx, req.Id) + } + safe, err := node.store.ReadSafe(ctx, req.Holder) + if err != nil { + return fmt.Errorf("store.ReadSafe(%s) => %v", req.Holder, err) + } else if safe != nil { + return node.store.FailRequest(ctx, req.Id) + } + old, err := node.store.ReadSafeProposal(ctx, req.Id) + if err != nil { + return fmt.Errorf("store.ReadSafeProposal(%s) => %v", req.Id, err) + } else if old != nil { + return node.store.FailRequest(ctx, req.Id) + } + + signer, observer, err := node.store.AssignSignerAndObserverToHolder(ctx, req, SafeKeyBackupMaturity, arp.Observer) + logger.Printf("store.AssignSignerAndObserverToHolder(%s) => %s %s %v", req.Holder, signer, observer, err) + if err != nil { + return fmt.Errorf("store.AssignSignerAndObserverToHolder(%v) => %v", req, err) + } + if signer == "" || observer == "" { + return node.refundAndFailRequest(ctx, req, arp.Receivers, int(arp.Threshold)) + } + if arp.Observer != "" && arp.Observer != observer { + return fmt.Errorf("store.AssignSignerAndObserverToHolder(%v) => %v %s", req, arp, observer) + } + if !common.CheckUnique(req.Holder, signer, observer) { + return node.refundAndFailRequest(ctx, req, arp.Receivers, int(arp.Threshold)) + } + + acc := mixin.BuildAddress(req.Holder, signer, observer) + logger.Verbosef("mixin.BuildAddress(%v) => %v", req, acc) + old, err = node.store.ReadSafeProposalByAddress(ctx, acc.String()) + if err != nil { + return fmt.Errorf("store.ReadSafeProposalByAddress(%s) => %v", acc.String(), err) + } else if old != nil { + return node.store.FailRequest(ctx, req.Id) + } + + exk := node.writeStorageUntilSnapshot(ctx, []byte(common.Base91Encode([]byte(acc.String())))) + typ := byte(common.ActionMixinKernelSafeProposeAccount) + err = node.sendObserverResponseWithReferences(ctx, req.Id, typ, req.Curve, exk) + if err != nil { + return fmt.Errorf("node.sendObserverResponse(%s, %x) => %v", req.Id, exk, err) + } + + path := mixinDefaultDerivationPath() + sp := &store.SafeProposal{ + RequestId: req.Id, + Chain: chain, + Holder: req.Holder, + Signer: signer, + Observer: observer, + Timelock: arp.Timelock, + Path: hex.EncodeToString(path), + Address: acc.String(), + Extra: acc.PrivateViewKey[:], + Receivers: arp.Receivers, + Threshold: arp.Threshold, + CreatedAt: req.CreatedAt, + UpdatedAt: req.CreatedAt, + } + return node.store.WriteSafeProposalWithRequest(ctx, sp) +} + +func (node *Node) processMixinKernelSafeApproveAccount(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleObserver { + panic(req.Role) + } + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Curve) + } + chain := byte(SafeChainMixinKernel) + old, err := node.store.ReadSafe(ctx, req.Holder) + if err != nil { + return fmt.Errorf("store.ReadSafe(%s) => %v", req.Holder, err) + } else if old != nil { + return node.store.FailRequest(ctx, req.Id) + } + + extra, _ := hex.DecodeString(req.Extra) + if len(extra) != 16+64 { + return node.store.FailRequest(ctx, req.Id) + } + rid, err := uuid.FromBytes(extra[:16]) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + sp, err := node.store.ReadSafeProposal(ctx, rid.String()) + if err != nil { + return fmt.Errorf("store.ReadSafeProposal(%v) => %s %v", req, rid.String(), err) + } else if sp == nil { + return node.store.FailRequest(ctx, req.Id) + } else if sp.Holder != req.Holder { + return node.store.FailRequest(ctx, req.Id) + } else if sp.Chain != chain { + return node.store.FailRequest(ctx, req.Id) + } + + ms := fmt.Sprintf("APPROVE:%s:%s", rid.String(), sp.Address) + msg := mixin.HashMessageForSignature(ms) + err = mixin.VerifySignature(req.Holder, msg, extra[16:]) + logger.Printf("mixin.VerifySignature(%v) => %v", req, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + + spr, err := node.store.ReadRequest(ctx, sp.RequestId) + if err != nil { + return fmt.Errorf("store.ReadRequest(%s) => %v", sp.RequestId, err) + } + exk := node.writeStorageUntilSnapshot(ctx, []byte(common.Base91Encode([]byte(sp.Address)))) + typ := byte(common.ActionMixinKernelSafeApproveAccount) + err = node.sendObserverResponseWithAssetAndReferences(ctx, req.Id, typ, req.Curve, spr.AssetId, spr.Amount.String(), exk) + if err != nil { + return fmt.Errorf("node.sendObserverResponse(%s, %x) => %v", req.Id, exk, err) + } + + safe := &store.Safe{ + Holder: sp.Holder, + Chain: sp.Chain, + Signer: sp.Signer, + Observer: sp.Observer, + Timelock: sp.Timelock, + Path: sp.Path, + Address: sp.Address, + Extra: sp.Extra, + Receivers: sp.Receivers, + Threshold: sp.Threshold, + RequestId: req.Id, + State: SafeStateApproved, + CreatedAt: req.CreatedAt, + UpdatedAt: req.CreatedAt, + } + return node.store.WriteSafeWithRequest(ctx, safe) +} + +func (node *Node) processMixinKernelSafeCloseAccount(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleObserver { + panic(req.Role) + } + panic(0) +} + +func (node *Node) processMixinKernelSafeProposeTransaction(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleHolder { + panic(req.Role) + } + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Curve) + } + chain := byte(SafeChainMixinKernel) + safe, err := node.store.ReadSafe(ctx, req.Holder) + if err != nil { + return fmt.Errorf("store.ReadSafe(%s) => %v", req.Holder, err) + } + if safe == nil || safe.Chain != chain { + return node.store.FailRequest(ctx, req.Id) + } + if safe.State != SafeStateApproved { + return node.store.FailRequest(ctx, req.Id) + } + + meta, err := node.fetchAssetMeta(ctx, req.AssetId) + logger.Printf("node.fetchAssetMeta(%s) => %v %v", req.AssetId, meta, err) + if err != nil { + return fmt.Errorf("node.fetchAssetMeta(%s) => %v", req.AssetId, err) + } + if meta.Chain != SafeChainMVM { + return node.store.FailRequest(ctx, req.Id) + } + deployed, err := abi.CheckFactoryAssetDeployed(node.conf.MVMRPC, meta.AssetKey) + logger.Printf("abi.CheckFactoryAssetDeployed(%s) => %v %v", meta.AssetKey, deployed, err) + if err != nil { + return fmt.Errorf("api.CheckFatoryAssetDeployed(%s) => %v", meta.AssetKey, err) + } + if deployed.Sign() <= 0 { + return node.store.FailRequest(ctx, req.Id) + } + assetId := uuid.Must(uuid.FromBytes(deployed.Bytes())) + + plan, err := node.store.ReadLatestOperationParams(ctx, safe.Chain, req.CreatedAt) + logger.Printf("store.ReadLatestOperationParams(%d) => %v %v", safe.Chain, plan, err) + if err != nil { + return fmt.Errorf("store.ReadLatestOperationParams(%d) => %v", safe.Chain, err) + } else if plan == nil || !plan.TransactionMinimum.IsPositive() { + return node.refundAndFailRequest(ctx, req, safe.Receivers, int(safe.Threshold)) + } + if req.Amount.Cmp(plan.TransactionMinimum) < 0 { + return node.store.FailRequest(ctx, req.Id) + } + + bondId, _, err := node.getBondAsset(ctx, assetId.String(), req.Holder) + logger.Printf("node.getBondAsset(%s, %s) => %s %v", assetId.String(), req.Holder, bondId, err) + if err != nil { + return fmt.Errorf("node.getBondAsset(%s, %s) => %v", assetId.String(), req.Holder, err) + } + if crypto.NewHash([]byte(req.AssetId)) != bondId { + return node.store.FailRequest(ctx, req.Id) + } + + extra, _ := hex.DecodeString(req.Extra) + if len(extra) < 33 { + return node.store.FailRequest(ctx, req.Id) + } + + switch extra[0] { + case common.FlagProposeNormalTransaction: + case common.FlagProposeRecoveryTransaction: + default: + return node.store.FailRequest(ctx, req.Id) + } + extra = extra[1:] + + var outputs []*mixin.Output + ver, _ := common.ReadKernelTransaction(node.conf.MixinRPC, req.MixinHash) + if len(extra) == 32 && len(ver.References) == 1 && ver.References[0].String() == hex.EncodeToString(extra) { + stx, _ := common.ReadKernelTransaction(node.conf.MixinRPC, ver.References[0]) + extra := common.DecodeMixinObjectExtra(stx.Extra) + var recipients [][2]string // TODO better encoding + err = json.Unmarshal(extra, &recipients) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + var total decimal.Decimal + for _, rp := range recipients { + addr, err := mixin.ParseAddress(rp[0]) + logger.Printf("mixin.ParseAddress(%s) => %v %v", string(extra), addr, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + amt, err := decimal.NewFromString(rp[1]) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + if amt.Cmp(plan.TransactionMinimum) < 0 { + return node.store.FailRequest(ctx, req.Id) + } + total = total.Add(amt) + outputs = append(outputs, &mixin.Output{ + Address: addr, + Amount: amt, + }) + } + if !total.Equal(req.Amount) { + return node.store.FailRequest(ctx, req.Id) + } + } else { + addr, err := mixin.ParseAddress(string(extra[16:])) + logger.Printf("mixin.ParseAddress(%s) => %v %v", string(extra), addr, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + outputs = append(outputs, &mixin.Output{ + Address: addr, + Amount: req.Amount, + }) + } + + allInputs, err := node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, safe.Holder, assetId.String()) + if err != nil { + return fmt.Errorf("store.ListAllMixinKernelUTXOsForHolderAndAsset(%s, %s) => %v", req.Holder, assetId.String(), err) + } + psbt, err := mixin.BuildPartiallySignedTransaction(allInputs, outputs, req.Id, safe.Holder, safe.Signer, safe.Observer) + logger.Printf("mixin.BuildPartiallySignedTransaction(%v) => %v %v", req, psbt, err) + if bitcoin.IsInsufficientInputError(err) { + return node.refundAndFailRequest(ctx, req, safe.Receivers, int(safe.Threshold)) + } + if err != nil { + return fmt.Errorf("mixin.BuildPartiallySignedTransaction(%v) => %v", req, err) + } + + msg := common.Base91Encode(psbt.PayloadMarshal()) + exk := node.writeStorageUntilSnapshot(ctx, []byte(msg)) + typ := byte(common.ActionMixinKernelSafeProposeTransaction) + err = node.sendObserverResponseWithReferences(ctx, req.Id, typ, req.Curve, exk) + if err != nil { + return fmt.Errorf("node.sendObserverResponse(%s, %x) => %v", req.Id, exk, err) + } + + total := decimal.Zero + recipients := make([]map[string]string, len(outputs)) + for i, out := range outputs { + recipients[i] = map[string]string{ + "receiver": out.Address.String(), + "amount": out.Amount.String(), + } + total = total.Add(out.Amount) + } + if !total.Equal(req.Amount) { + return node.store.FailRequest(ctx, req.Id) + } + data := common.MarshalJSONOrPanic(map[string]any{ + "recipients": recipients, + "storage": exk, + }) + tx := &store.Transaction{ + TransactionHash: psbt.PayloadHash().String(), + RawTransaction: hex.EncodeToString(psbt.PayloadMarshal()), + Holder: req.Holder, + Chain: safe.Chain, + AssetId: assetId.String(), + State: common.RequestStateInitial, + Data: string(data), + RequestId: req.Id, + CreatedAt: req.CreatedAt, + UpdatedAt: req.CreatedAt, + } + inputs := make([]*store.TransactionInput, len(psbt.Inputs)) + for i, in := range psbt.Inputs { + inputs[i] = &store.TransactionInput{ + Hash: in.Hash.String(), + Index: uint32(in.Index), + } + } + return node.store.WriteTransactionWithRequest(ctx, tx, inputs) +} + +func (node *Node) processMixinKernelSafeRevokeTransaction(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleObserver { + panic(req.Role) + } + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Curve) + } + chain := byte(SafeChainMixinKernel) + safe, err := node.store.ReadSafe(ctx, req.Holder) + if err != nil { + return fmt.Errorf("store.ReadSafe(%s) => %v", req.Holder, err) + } + if safe == nil || safe.Chain != chain { + return node.store.FailRequest(ctx, req.Id) + } + + extra, _ := hex.DecodeString(req.Extra) + if len(extra) < 64 { + return node.store.FailRequest(ctx, req.Id) + } + rid, err := uuid.FromBytes(extra[:16]) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + tx, err := node.store.ReadTransactionByRequestId(ctx, rid.String()) + if err != nil { + return fmt.Errorf("store.ReadTransactionByRequestId(%v) => %s %v", req, rid.String(), err) + } else if tx == nil { + return node.store.FailRequest(ctx, req.Id) + } else if tx.Holder != req.Holder { + return node.store.FailRequest(ctx, req.Id) + } else if tx.State != common.RequestStateInitial { + return node.store.FailRequest(ctx, req.Id) + } + + ms := fmt.Sprintf("REVOKE:%s:%s", rid.String(), tx.TransactionHash) + msg := mixin.HashMessageForSignature(ms) + err = mixin.VerifySignature(req.Holder, msg, extra[16:]) + logger.Printf("holder: mixin.VerifySignature(%v) => %v", req, err) + if err != nil { + err = mixin.VerifySignature(safe.Observer, msg, extra[16:]) + logger.Printf("observer: mixin.VerifySignature(%v) => %v", req, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + } + + bondId, _, err := node.getBondAsset(ctx, tx.AssetId, safe.Holder) + logger.Printf("node.getBondAsset(%s, %s) => %s %v", tx.AssetId, req.Holder, bondId, err) + if err != nil { + return fmt.Errorf("node.getBondAsset(%s, %s) => %v", tx.AssetId, req.Holder, err) + } + var data struct { + Recipients []map[string]string `json:"recipients"` + } + err = json.Unmarshal([]byte(tx.Data), &data) + if err != nil { + panic(err) + } + amount := decimal.Zero + for _, t := range data.Recipients { + ta := decimal.RequireFromString(t["amount"]) + if ta.Cmp(decimal.NewFromFloat(0.0001)) < 0 { + panic(tx.Data) + } + amount = amount.Add(ta) + } + meta, err := node.fetchAssetMeta(ctx, bondId.String()) + logger.Printf("node.fetchAssetMeta(%s) => %v %v", bondId.String(), meta, err) + if err != nil { + return fmt.Errorf("node.fetchAssetMeta(%s) => %v", bondId.String(), err) + } + if meta.Chain != SafeChainMVM { + return node.store.FailRequest(ctx, req.Id) + } + err = node.buildTransaction(ctx, meta.AssetId, safe.Receivers, int(safe.Threshold), amount.String(), nil, req.Id) + if err != nil { + return err + } + + return node.store.RevokeTransactionWithRequest(ctx, tx, safe, req) +} + +func (node *Node) processMixinKernelSafeApproveTransaction(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleObserver { + panic(req.Role) + } + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Curve) + } + chain := byte(SafeChainMixinKernel) + safe, err := node.store.ReadSafe(ctx, req.Holder) + if err != nil { + return fmt.Errorf("store.ReadSafe(%s) => %v", req.Holder, err) + } + if safe == nil || safe.Chain != chain { + return node.store.FailRequest(ctx, req.Id) + } + + extra, _ := hex.DecodeString(req.Extra) + if len(extra) != 16+64 { + return node.store.FailRequest(ctx, req.Id) + } + rid, err := uuid.FromBytes(extra[:16]) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + tx, err := node.store.ReadTransactionByRequestId(ctx, rid.String()) + if err != nil { + return fmt.Errorf("store.ReadTransactionByRequestId(%v) => %s %v", req, rid.String(), err) + } else if tx == nil { + return node.store.FailRequest(ctx, req.Id) + } else if tx.Holder != req.Holder { + return node.store.FailRequest(ctx, req.Id) + } + + ms := fmt.Sprintf("APPROVE:%s:%s", rid.String(), tx.TransactionHash) + msg := mixin.HashMessageForSignature(ms) + err = mixin.VerifySignature(tx.Holder, msg, extra[16:]) + logger.Printf("mixin.VerifySignature(%v) => %v", req, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + + var data struct { + StorageTransaction crypto.Hash `json:"storage"` + } + err = json.Unmarshal([]byte(tx.Data), &data) + if err != nil { + panic(err) + } + + raw := common.DecodeHexOrPanic(tx.RawTransaction) + psbt, err := mixin.ParsePartiallySignedTransaction(raw) + if err != nil { + panic(err) + } + addr := mixin.BuildAddress(safe.Holder, safe.Signer, safe.Observer) + var requests []*store.SignatureRequest + for idx, in := range psbt.Inputs { + pending, err := node.checkTransactionIndexSignaturePending(ctx, tx.TransactionHash, idx, req) + logger.Printf("node.checkTransactionIndexSignaturePending(%s, %d) => %t %v", tx.TransactionHash, idx, pending, err) + if err != nil { + return err + } else if pending { + continue + } + + utxo, _, _ := node.store.ReadMixinKernelUTXO(ctx, in.Hash.String(), in.Index) + r := crypto.KeyMultPubPriv(&utxo.Mask, &addr.PrivateViewKey) + msg := crypto.HashScalar(r, uint64(in.Index)).Bytes() + msg = append(msg, data.StorageTransaction[:]...) + + sr := &store.SignatureRequest{ + TransactionHash: tx.TransactionHash, + InputIndex: idx, + Signer: safe.Signer, + Curve: req.Curve, + Message: hex.EncodeToString(msg), + State: common.RequestStateInitial, + CreatedAt: req.CreatedAt, + UpdatedAt: req.CreatedAt, + } + sr.RequestId = common.UniqueId(req.Id, sr.Message) + requests = append(requests, sr) + } + err = node.store.WriteSignatureRequestsWithRequest(ctx, requests, tx.TransactionHash, req) + logger.Printf("store.WriteSignatureRequestsWithRequest(%s, %d, %v) => %v", tx.TransactionHash, len(requests), req, err) + if err != nil { + return fmt.Errorf("store.WriteSignatureRequestsWithRequest(%s) => %v", tx.TransactionHash, err) + } + + for _, sr := range requests { + err := node.sendSignerSignRequest(ctx, sr, safe.Path) + if err != nil { + return fmt.Errorf("node.sendSignerSignRequest(%v) => %v", sr, err) + } + } + return nil +} + +func (node *Node) processMixinKernelSafeSignatureResponse(ctx context.Context, req *common.Request) error { + if req.Role != common.RequestRoleSigner { + panic(req.Role) + } + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Curve) + } + old, err := node.store.ReadSignatureRequest(ctx, req.Id) + logger.Printf("store.ReadSignatureRequest(%s) => %v %v", req.Id, old, err) + if err != nil { + return fmt.Errorf("store.ReadSignatureRequest(%s) => %v", req.Id, err) + } + if old == nil || old.State == common.RequestStateDone { + return node.store.FailRequest(ctx, req.Id) + } + tx, err := node.store.ReadTransaction(ctx, old.TransactionHash) + if err != nil { + return fmt.Errorf("store.ReadTransaction(%v) => %s %v", req, old.TransactionHash, err) + } + safe, err := node.store.ReadSafe(ctx, tx.Holder) + if err != nil { + return fmt.Errorf("store.ReadSafe(%s) => %v", tx.Holder, err) + } + if safe.Signer != req.Holder { + return node.store.FailRequest(ctx, req.Id) + } + + raw := common.DecodeHexOrPanic(tx.RawTransaction) + sig, _ := hex.DecodeString(req.Extra) + msg := common.DecodeHexOrPanic(old.Message) + spk := mixin.DeriveKey(safe.Signer, msg[:32]) + err = mixin.VerifySignature(spk, raw, sig) + logger.Printf("mixin.VerifySignature(%v) => %v", req, err) + if err != nil { + return node.store.FailRequest(ctx, req.Id) + } + + err = node.store.FinishSignatureRequest(ctx, req) + logger.Printf("store.FinishSignatureRequest(%s) => %v", req.Id, err) + if err != nil { + return fmt.Errorf("store.FinishSignatureRequest(%s) => %v", req.Id, err) + } + + requests, err := node.store.ListAllSignaturesForTransaction(ctx, old.TransactionHash, common.RequestStatePending) + logger.Printf("store.ListAllSignaturesForTransaction(%s) => %d %v", old.TransactionHash, len(requests), err) + if err != nil { + return fmt.Errorf("store.ListAllSignaturesForTransaction(%s) => %v", old.TransactionHash, err) + } + + psbt, err := mixin.ParsePartiallySignedTransaction(raw) + if err != nil { + panic(err) + } + for idx := range psbt.Inputs { + sr := requests[idx] + if sr == nil { + return node.store.FailRequest(ctx, req.Id) + } + msg := common.DecodeHexOrPanic(sr.Message) + sig := common.DecodeHexOrPanic(sr.Signature.String) + spk := mixin.DeriveKey(safe.Signer, msg[:32]) + err = mixin.VerifySignature(spk, raw, sig) + if err != nil { + panic(sr.Signature.String) + } + var msig crypto.Signature + copy(msig[:], sig) + psbt.SignaturesMap = append(psbt.SignaturesMap, map[uint16]*crypto.Signature{0: &msig}) + } + + exk := node.writeStorageUntilSnapshot(ctx, []byte(common.Base91Encode(psbt.Marshal()))) + id := common.UniqueId(old.TransactionHash, hex.EncodeToString(exk[:])) + typ := byte(common.ActionMixinKernelSafeApproveTransaction) + err = node.sendObserverResponseWithReferences(ctx, id, typ, req.Curve, exk) + if err != nil { + return fmt.Errorf("node.sendObserverResponse(%s, %x) => %v", id, exk, err) + } + signed := hex.EncodeToString(psbt.Marshal()) + err = node.store.FinishTransactionSignaturesWithRequest(ctx, old.TransactionHash, signed, req, int64(len(psbt.Inputs)), safe) + logger.Printf("store.FinishTransactionSignaturesWithRequest(%s, %s, %v) => %v", old.TransactionHash, signed, req, err) + return err +} + +func (node *Node) checkMixinKernelChange(ctx context.Context, deposit *Deposit, mtx *common.VersionedTransaction) (bool, error) { + vin, spentBy, err := node.store.ReadMixinKernelUTXO(ctx, mtx.Inputs[0].Hash.String(), mtx.Inputs[0].Index) + if err != nil || vin == nil { + return false, err + } + tx, err := node.store.ReadTransaction(ctx, spentBy) + if err != nil { + return false, err + } + var recipients []map[string]string + err = json.Unmarshal([]byte(tx.Data), &recipients) + if err != nil || len(recipients) == 0 { + return false, fmt.Errorf("store.ReadTransaction(%s) => %s", spentBy, tx.Data) + } + return deposit.Index >= uint64(len(recipients)), nil +} + +func (node *Node) verifyMixinKernelTransaction(ctx context.Context, req *common.Request, deposit *Deposit, mtx *common.VersionedTransaction, safe *store.Safe) (*mixin.Input, error) { + input, receiver := mixin.ParseTransactionDepositOutput(safe.Holder, safe.Signer, safe.Observer, mtx, int(deposit.Index)) + if input == nil { + return nil, fmt.Errorf("malicious mixin kernel deposit or node not in sync? %s %d", deposit.Hash, deposit.Index) + } + if input.Asset != crypto.NewHash([]byte(deposit.Asset)) { + return nil, fmt.Errorf("malicious mixin kernel deposit asset %s %d", deposit.Hash, deposit.Index) + } + + if !input.Amount.Equal(decimal.NewFromBigInt(deposit.Amount, -mixin.ValuePrecision)) { + return nil, fmt.Errorf("malicious mixin kernel deposit amount %s %d", deposit.Hash, deposit.Index) + } + if safe.Address != receiver { + return nil, fmt.Errorf("malicious mixin kernel deposit address %s %d", deposit.Hash, deposit.Index) + } + + return input, nil +} diff --git a/keeper/mixin_test.go b/keeper/mixin_test.go new file mode 100644 index 00000000..4637422a --- /dev/null +++ b/keeper/mixin_test.go @@ -0,0 +1,517 @@ +package keeper + +import ( + "context" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/MixinNetwork/mixin/crypto" + "github.com/MixinNetwork/mixin/logger" + "github.com/MixinNetwork/safe/apps/bitcoin" + "github.com/MixinNetwork/safe/apps/mixin" + "github.com/MixinNetwork/safe/common" + "github.com/MixinNetwork/safe/signer" + "github.com/MixinNetwork/trusted-group/mtg" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "github.com/gofrs/uuid/v5" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/require" +) + +const ( + testMixinKernelAddress = "XINZrJcfd6QoKrR7Q31YY7gk2zvbU1qkAAZ4xBan4KQYeDq7sZ21g4WFsa2bXKoXSWy4sRvr8grSBRmXPxjDBHbF4jaik5om" + testMixinKernelHolderPrivate = "6761ad91d9784b92b14f6be83fdd364a0a722367bbbf8525a95d4153bf8e7008" + testMixinKernelObserverPrivate = "218a231184dc3be90183118b83f854df3057a53f7a9edb886a12d434ed8fcb06" + testMixinKernelDummyHolderPrivate = "1000c89522d07e0acf4bf65c1d07f662e7d6412c49d4881818817b5726d1a802" + testMixinKernelBondAssetId = "afd17288-1765-3d37-ba91-43bb73448ae0" + testMixinKernelTransactionReceiver = "XINZrJcfd6QoKrR7Q31YY7gk2zvbU1qkAAZ4xBan4KQYeDq7sZ21g4WFsa2bXKoXSWy4sRvr8grSBRmXPxjDBHbF4jaik5om" +) + +func TestMixinKeeper(t *testing.T) { + require := require.New(t) + ctx, node, mpc, signers := testMixinKernelPrepare(require) + + observer := testMixinKernelPublicKey(testMixinKernelObserverPrivate) + bondId := testDeployBondContract(ctx, require, node, testMixinKernelAddress, SafeMixinKernelAssetId) + require.Equal(testMixinKernelBondAssetId, bondId) + node.ProcessOutput(ctx, &mtg.Output{AssetID: bondId, Amount: decimal.NewFromInt(1000000), CreatedAt: time.Now()}) + input := &mixin.Input{ + TransactionHash: "74e131b1224af9cb3d644eaf10ed6ee8e9af1dc73981bfc61f3e6cb8d4d4c7e2", + Index: 0, + Amount: decimal.RequireFromString("0.000123"), + } + testMixinKernelObserverHolderDeposit(ctx, require, node, mpc, observer, input, 1) + input = &mixin.Input{ + TransactionHash: "74e131b1224af9cb3d644eaf10ed6ee8e9af1dc73981bfc61f3e6cb8d4d4c7e2", + Index: 1, + Amount: decimal.RequireFromString("0.006877"), + } + testMixinKernelObserverHolderDeposit(ctx, require, node, mpc, observer, input, 2) + + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + outputs, err := node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(outputs, 2) + pendings, err := node.store.ListPendingMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(pendings, 0) + + transactionHash := testMixinKernelProposeTransaction(ctx, require, node, mpc, bondId, "3e37ea1c-1455-400d-9642-f6bbcd8c744e", "c330608a509b84a6cc8063249145dbdd880885f8c8c1f6593400a37b399c5f62", "77770004a99c2e0e2b1da4d648755ef19bd95139acbbe6564cfb06dec7cd34931ca72cdc000274e131b1224af9cb3d644eaf10ed6ee8e9af1dc73981bfc61f3e6cb8d4d4c7e2000000000000000074e131b1224af9cb3d644eaf10ed6ee8e9af1dc73981bfc61f3e6cb8d4d4c7e20001000000000000000200000002300c000120253714361d28713900f1b6b09d4b38565fa041cbb4b20da8e8e3169ced26b1a11e825b186b695bc72cdbd312ca8b03457d4c592e33dfd66fa46d226dcfcc220003fffe010000000000030a7e54000162ddce376ba321f5766842f5953b6896992d8d515fb2061247972f6332a1c5b82ef07340f5fd41b9fc5f106bb0df32b651c2ccee1ba66ccb9fc165da60da696d0003fffe0100000000000000103e37ea1c1455400d9642f6bbcd8c744e0000") + outputs, err = node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(outputs, 0) + pendings, err = node.store.ListPendingMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(pendings, 2) + testMixinKernelRevokeTransaction(ctx, require, node, transactionHash, false) + outputs, err = node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(outputs, 2) + pendings, err = node.store.ListPendingMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(pendings, 0) + + transactionHash = testMixinKernelProposeTransaction(ctx, require, node, mpc, bondId, "b0a22078-0a86-459d-93f4-a1aadbf2b9b7", "633a03e24ec62f28ff1e2769fc98a18dc8141c61db5ab7fdec240e546a384cdc", "77770004a99c2e0e2b1da4d648755ef19bd95139acbbe6564cfb06dec7cd34931ca72cdc000274e131b1224af9cb3d644eaf10ed6ee8e9af1dc73981bfc61f3e6cb8d4d4c7e2000000000000000074e131b1224af9cb3d644eaf10ed6ee8e9af1dc73981bfc61f3e6cb8d4d4c7e20001000000000000000200000002300c0001317c78dd1f1ff39ed14c0ba595536da85c04ffcfc4c349dcb18651c254be3f41369e617a18e7655e927e5f0f3805f5fd8d182888556311f626f441b02ac81a010003fffe010000000000030a7e5400011af4766dbf46790d9d87e0427421b0ce6d9720c8d8327f8c3390eadcb48ac8d196e7f0d15190e22a1c07ed9faa9ecfa54c88b4bc77539ab7afc977b6a541ed690003fffe010000000000000010b0a220780a86459d93f4a1aadbf2b9b70000") + outputs, err = node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(outputs, 0) + pendings, err = node.store.ListPendingMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(pendings, 2) + testMixinKernelApproveTransaction(ctx, require, node, transactionHash, signers) + outputs, err = node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(outputs, 0) + pendings, err = node.store.ListPendingMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(pendings, 0) + testSpareKeys(ctx, require, node, 0, 0, 0, common.CurveEdwards25519Mixin) +} + +func testMixinKernelPrepare(require *require.Assertions) (context.Context, *Node, string, []*signer.Node) { + logger.SetLevel(logger.VERBOSE) + ctx, signers := signer.TestPrepare(require) + mpc := signer.TestFROSTPrepareKeys(ctx, require, signers, common.CurveEdwards25519Mixin) + chainCode := make([]byte, 32) + + root, err := os.MkdirTemp("", "safe-keeper-test-") + require.Nil(err) + node := testBuildNode(ctx, require, root) + require.NotNil(node) + timestamp, err := node.timestamp(ctx) + require.Nil(err) + require.Equal(time.Unix(0, node.conf.MTG.Genesis.Timestamp), timestamp) + testSpareKeys(ctx, require, node, 0, 0, 0, common.CurveEdwards25519Mixin) + + id := uuid.Must(uuid.NewV4()).String() + extra := append([]byte{common.RequestRoleSigner}, chainCode...) + extra = append(extra, common.RequestFlagNone) + out := testBuildSignerOutput(node, id, mpc, common.OperationTypeKeygenOutput, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + v, err := node.store.ReadProperty(ctx, id) + require.Nil(err) + require.Equal("", v) + testSpareKeys(ctx, require, node, 0, 1, 0, common.CurveEdwards25519Mixin) + + id = uuid.Must(uuid.NewV4()).String() + observer := testMixinKernelPublicKey(testMixinKernelObserverPrivate) + occ := make([]byte, 32) + extra = append([]byte{common.RequestRoleObserver}, occ...) + extra = append(extra, common.RequestFlagNone) + out = testBuildObserverRequest(node, id, observer, common.ActionObserverAddKey, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + v, err = node.store.ReadProperty(ctx, id) + require.Nil(err) + require.Equal("", v) + testSpareKeys(ctx, require, node, 0, 1, 1, common.CurveEdwards25519Mixin) + + batch := byte(64) + id = uuid.Must(uuid.NewV4()).String() + dummy := testMixinKernelPublicKey(testMixinKernelDummyHolderPrivate) + out = testBuildObserverRequest(node, id, dummy, common.ActionObserverRequestSignerKeys, []byte{batch}, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + for i := byte(0); i < batch; i++ { + pid := common.UniqueId(id, fmt.Sprintf("%8d", i)) + pid = common.UniqueId(pid, fmt.Sprintf("MTG:%v:%d", node.signer.Genesis.Members, node.signer.Genesis.Threshold)) + v, _ := node.store.ReadProperty(ctx, pid) + var om map[string]any + json.Unmarshal([]byte(v), &om) + b, _ := hex.DecodeString(om["memo"].(string)) + b = common.AESDecrypt(node.signerAESKey[:], b) + o, err := common.DecodeOperation(b) + require.Nil(err) + require.Equal(pid, o.Id) + } + testSpareKeys(ctx, require, node, 0, 1, 1, common.CurveEdwards25519Mixin) + + for i := 0; i < 10; i++ { + testMixinKernelUpdateAccountPrice(ctx, require, node) + } + rid, publicKey := testMixinKernelProposeAccount(ctx, require, node, mpc, observer) + testSpareKeys(ctx, require, node, 0, 0, 0, common.CurveEdwards25519Mixin) + testMixinKernelApproveAccount(ctx, require, node, mpc, observer, rid, publicKey) + testSpareKeys(ctx, require, node, 0, 0, 0, common.CurveEdwards25519Mixin) + for i := 0; i < 10; i++ { + testMixinKernelUpdateNetworkStatus(ctx, require, node, 641117557, "2192715566293aba968675bd63a211d5489e283c2facfb19456bb51d75b80df6") + } + + return ctx, node, mpc, signers +} + +func testMixinKernelUpdateAccountPrice(ctx context.Context, require *require.Assertions, node *Node) { + id := uuid.Must(uuid.NewV4()).String() + + extra := []byte{SafeChainMixinKernel} + extra = append(extra, uuid.Must(uuid.FromString(testAccountPriceAssetId)).Bytes()...) + extra = binary.BigEndian.AppendUint64(extra, testAccountPriceAmount*100000000) + extra = binary.BigEndian.AppendUint64(extra, 10000) + dummy := testMixinKernelPublicKey(testMixinKernelDummyHolderPrivate) + out := testBuildObserverRequest(node, id, dummy, common.ActionObserverSetOperationParams, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + + plan, err := node.store.ReadLatestOperationParams(ctx, SafeChainMixinKernel, time.Now()) + require.Nil(err) + require.Equal(testAccountPriceAssetId, plan.OperationPriceAsset) + require.Equal(fmt.Sprint(testAccountPriceAmount), plan.OperationPriceAmount.String()) + require.Equal("0.0001", plan.TransactionMinimum.String()) +} + +func testMixinKernelUpdateNetworkStatus(ctx context.Context, require *require.Assertions, node *Node, blockHeight int, blockHash string) { + id := uuid.Must(uuid.NewV4()).String() + fee, height := 0, uint64(blockHeight) + hash, _ := crypto.HashFromString(blockHash) + + extra := []byte{SafeChainMixinKernel} + extra = binary.BigEndian.AppendUint64(extra, uint64(fee)) + extra = binary.BigEndian.AppendUint64(extra, height) + extra = append(extra, hash[:]...) + dummy := testMixinKernelPublicKey(testMixinKernelDummyHolderPrivate) + out := testBuildObserverRequest(node, id, dummy, common.ActionObserverUpdateNetworkStatus, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + + info, err := node.store.ReadLatestNetworkInfo(ctx, SafeChainMixinKernel, time.Now()) + require.Nil(err) + require.NotNil(info) + require.Equal(byte(SafeChainMixinKernel), info.Chain) + require.Equal(uint64(fee), info.Fee) + require.Equal(height, info.Height) + require.Equal(hash.String(), info.Hash) +} + +func testMixinKernelRevokeTransaction(ctx context.Context, require *require.Assertions, node *Node, transactionHash string, signByObserver bool) { + id := uuid.Must(uuid.NewV4()).String() + + tx, _ := node.store.ReadTransaction(ctx, transactionHash) + require.Equal(common.RequestStateInitial, tx.State) + + var sig crypto.Signature + ms := fmt.Sprintf("REVOKE:%s:%s", tx.RequestId, tx.TransactionHash) + msg := mixin.HashMessageForSignature(ms) + if signByObserver { + key, _ := crypto.KeyFromString(testMixinKernelObserverPrivate) + sig = key.Sign(msg) + } else { + key, _ := crypto.KeyFromString(testMixinKernelHolderPrivate) + sig = key.Sign(msg) + } + extra := uuid.Must(uuid.FromString(tx.RequestId)).Bytes() + extra = append(extra, sig[:]...) + + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + out := testBuildObserverRequest(node, id, holder, common.ActionMixinKernelSafeRevokeTransaction, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + requests, err := node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStateInitial) + require.Nil(err) + require.Len(requests, 0) + tx, _ = node.store.ReadTransaction(ctx, transactionHash) + require.Equal(common.RequestStateFailed, tx.State) +} + +func testMixinKernelApproveTransaction(ctx context.Context, require *require.Assertions, node *Node, transactionHash string, signers []*signer.Node) string { + id := uuid.Must(uuid.NewV4()).String() + + tx, _ := node.store.ReadTransaction(ctx, transactionHash) + require.Equal(common.RequestStateInitial, tx.State) + safe, _ := node.store.ReadSafe(ctx, tx.Holder) + + var data struct { + StorageTransaction crypto.Hash `json:"storage"` + } + json.Unmarshal([]byte(tx.Data), &data) + v, _ := node.store.ReadProperty(ctx, data.StorageTransaction.String()) + for _, sn := range signers { + signer.TestWriteProperty(ctx, sn, data.StorageTransaction.String(), v) + } + + key, _ := crypto.KeyFromString(testMixinKernelHolderPrivate) + ms := fmt.Sprintf("APPROVE:%s:%s", tx.RequestId, tx.TransactionHash) + sig := key.Sign(mixin.HashMessageForSignature(ms)) + extra := uuid.Must(uuid.FromString(tx.RequestId)).Bytes() + extra = append(extra, sig[:]...) + + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + out := testBuildObserverRequest(node, id, holder, common.ActionMixinKernelSafeApproveTransaction, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + requests, err := node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStateInitial) + require.Nil(err) + require.Len(requests, 2) + tx, _ = node.store.ReadTransaction(ctx, transactionHash) + require.Equal(common.RequestStatePending, tx.State) + + msg, _ := hex.DecodeString(requests[0].Message) + out = testBuildSignerOutput(node, requests[0].RequestId, safe.Signer, common.OperationTypeSignInput, msg, common.CurveEdwards25519Mixin) + for _, sn := range signers { + signer.TestWriteProperty(ctx, sn, out.TransactionHash.String(), data.StorageTransaction.String()) + } + op := signer.TestProcessOutput(ctx, require, signers, out, requests[0].RequestId) + out = testBuildSignerOutput(node, requests[0].RequestId, safe.Signer, common.OperationTypeSignOutput, op.Extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + requests, _ = node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStatePending) + require.Len(requests, 1) + requests, _ = node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStateInitial) + require.Len(requests, 1) + tx, _ = node.store.ReadTransaction(ctx, transactionHash) + require.Equal(common.RequestStatePending, tx.State) + + msg, _ = hex.DecodeString(requests[1].Message) + out = testBuildSignerOutput(node, requests[1].RequestId, safe.Signer, common.OperationTypeSignInput, msg, common.CurveEdwards25519Mixin) + for _, sn := range signers { + signer.TestWriteProperty(ctx, sn, out.TransactionHash.String(), data.StorageTransaction.String()) + } + op = signer.TestProcessOutput(ctx, require, signers, out, requests[1].RequestId) + out = testBuildSignerOutput(node, requests[1].RequestId, safe.Signer, common.OperationTypeSignOutput, op.Extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + requests, _ = node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStateInitial) + require.Len(requests, 0) + requests, _ = node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStatePending) + require.Len(requests, 0) + requests, _ = node.store.ListAllSignaturesForTransaction(ctx, transactionHash, common.RequestStateDone) + require.Len(requests, 2) + tx, _ = node.store.ReadTransaction(ctx, transactionHash) + require.Equal(common.RequestStateDone, tx.State) + + signed := make(map[int][]byte) + for _, r := range requests { + b, _ := hex.DecodeString(r.Signature.String) + signed[r.InputIndex] = b + } + mb := common.DecodeHexOrPanic(tx.RawTransaction) + exk := crypto.Blake3Hash([]byte(common.Base91Encode(mb))) + rid := common.UniqueId(transactionHash, hex.EncodeToString(exk[:])) + b := testReadObserverResponse(ctx, require, node, rid, common.ActionMixinKernelSafeApproveTransaction) + require.Equal(mb, b) + + tx, _ = node.store.ReadTransaction(ctx, tx.TransactionHash) + ver, _ := mixin.ParsePartiallySignedTransaction(common.DecodeHexOrPanic(tx.RawTransaction)) + require.Len(ver.SignaturesMap, 2) + require.Equal(signed[0], ver.SignaturesMap[0][0][:]) + require.Equal(signed[1], ver.SignaturesMap[1][0][:]) + signedRaw := hex.EncodeToString(ver.Marshal()) + logger.Println(signedRaw) + return signedRaw +} + +func testMixinKernelProposeTransaction(ctx context.Context, require *require.Assertions, node *Node, signer, bondId string, rid, rhash, rraw string) string { + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + observer := testMixinKernelPublicKey(testMixinKernelObserverPrivate) + info, _ := node.store.ReadLatestNetworkInfo(ctx, SafeChainMixinKernel, time.Now()) + extra := []byte{0} + extra = append(extra, uuid.Must(uuid.FromString(info.RequestId)).Bytes()...) + extra = append(extra, []byte(testMixinKernelTransactionReceiver)...) + out := testBuildHolderRequest(node, rid, holder, common.ActionMixinKernelSafeProposeTransaction, bondId, extra, decimal.NewFromFloat(0.000123)) + testStep(ctx, require, node, out) + + var view crypto.Key + safe, _ := node.store.ReadSafe(ctx, holder) + copy(view[:], safe.Extra) + addr := mixin.BuildAddress(holder, signer, observer) + require.Equal(view, addr.PrivateViewKey) + + b := testReadObserverResponse(ctx, require, node, rid, common.ActionMixinKernelSafeProposeTransaction) + require.Equal(rraw, hex.EncodeToString(b)) + psbt, err := mixin.ParsePartiallySignedTransaction(b) + require.Nil(err) + require.Equal(rhash, psbt.PayloadHash().String()) + + require.Len(psbt.Outputs, 2) + main := psbt.Outputs[0] + require.Equal("0.00012300", main.Amount.String()) + require.Len(main.Keys, 1) + pub := crypto.ViewGhostOutputKey(main.Keys[0], &view, &main.Mask, 0) + require.Equal(signer, pub.String()) + change := psbt.Outputs[1] + require.Equal("0.00687700", change.Amount.String()) + require.Len(change.Keys, 1) + pub = crypto.ViewGhostOutputKey(change.Keys[0], &view, &change.Mask, 1) + require.Equal(signer, pub.String()) + + stx, err := node.store.ReadTransaction(ctx, psbt.PayloadHash().String()) + require.Nil(err) + require.Equal(hex.EncodeToString(psbt.Marshal()), stx.RawTransaction) + require.Equal(common.RequestStateInitial, stx.State) + + if rid == "3e37ea1c-1455-400d-9642-f6bbcd8c744e" { + require.Equal("a11e825b186b695bc72cdbd312ca8b03457d4c592e33dfd66fa46d226dcfcc22", main.Mask.String()) + require.Equal("2ef07340f5fd41b9fc5f106bb0df32b651c2ccee1ba66ccb9fc165da60da696d", change.Mask.String()) + require.Equal("{\"recipients\":[{\"amount\":\"0.000123\",\"receiver\":\"XINZrJcfd6QoKrR7Q31YY7gk2zvbU1qkAAZ4xBan4KQYeDq7sZ21g4WFsa2bXKoXSWy4sRvr8grSBRmXPxjDBHbF4jaik5om\"}],\"storage\":\"08b87919fb1b36270bffbc8a21ad7d9d1175ec7eb37d9cc84c3d4e5a96a0cbf0\"}", stx.Data) + } else { + require.Equal("369e617a18e7655e927e5f0f3805f5fd8d182888556311f626f441b02ac81a01", main.Mask.String()) + require.Equal("96e7f0d15190e22a1c07ed9faa9ecfa54c88b4bc77539ab7afc977b6a541ed69", change.Mask.String()) + require.Equal("{\"recipients\":[{\"amount\":\"0.000123\",\"receiver\":\"XINZrJcfd6QoKrR7Q31YY7gk2zvbU1qkAAZ4xBan4KQYeDq7sZ21g4WFsa2bXKoXSWy4sRvr8grSBRmXPxjDBHbF4jaik5om\"}],\"storage\":\"0f692ca9b1152706967873513dc9e518ab51b1acbf1245e5e592dce332b5ff73\"}", stx.Data) + } + return stx.TransactionHash +} + +func testMixinKernelHolderApproveTransaction(rawTransaction string) string { + hb := common.DecodeHexOrPanic(testBitcoinKeyHolderPrivate) + holder, _ := btcec.PrivKeyFromBytes(hb) + + psTx, _ := bitcoin.UnmarshalPartiallySignedTransaction(common.DecodeHexOrPanic(rawTransaction)) + for idx := range psTx.UnsignedTx.TxIn { + hash := psTx.SigHash(idx) + sig := ecdsa.Sign(holder, hash).Serialize() + psTx.Inputs[idx].PartialSigs = []*psbt.PartialSig{{ + PubKey: holder.PubKey().SerializeCompressed(), + Signature: sig, + }} + } + raw := psTx.Marshal() + return hex.EncodeToString(raw) +} + +func (node *Node) testMixinKernelSignerHolderApproveTransaction(ctx context.Context, require *require.Assertions, rawTransaction string, signed map[int][]byte, signer, path string) *wire.MsgTx { + hb := common.DecodeHexOrPanic(testBitcoinKeyHolderPrivate) + holder, _ := btcec.PrivKeyFromBytes(hb) + + b, _ := hex.DecodeString(rawTransaction) + psbt, _ := bitcoin.UnmarshalPartiallySignedTransaction(b) + msgTx := psbt.UnsignedTx + for idx := range msgTx.TxIn { + pop := msgTx.TxIn[idx].PreviousOutPoint + hash := psbt.SigHash(idx) + utxo, _, _ := node.store.ReadBitcoinUTXO(ctx, pop.Hash.String(), int(pop.Index)) + msig := signed[idx] + if msig == nil { + continue + } + + msig = append(msig, byte(bitcoin.SigHashType)) + der, _ := ecdsa.ParseDERSignature(msig[:len(msig)-1]) + pub, _ := node.deriveBIP32WithPath(ctx, signer, common.DecodeHexOrPanic(path)) + signer, _ := btcutil.NewAddressPubKey(common.DecodeHexOrPanic(pub), &chaincfg.MainNetParams) + require.True(der.Verify(hash, signer.PubKey())) + + msgTx.TxIn[idx].Witness = append(msgTx.TxIn[idx].Witness, []byte{}) + msgTx.TxIn[idx].Witness = append(msgTx.TxIn[idx].Witness, msig) + + signature := ecdsa.Sign(holder, hash) + sig := append(signature.Serialize(), byte(bitcoin.SigHashType)) + msgTx.TxIn[idx].Witness = append(msgTx.TxIn[idx].Witness, sig) + msgTx.TxIn[idx].Witness = append(msgTx.TxIn[idx].Witness, utxo.Script) + } + + return msgTx +} + +func testMixinKernelObserverHolderDeposit(ctx context.Context, require *require.Assertions, node *Node, signer, observer string, input *mixin.Input, t int) { + id := uuid.Must(uuid.NewV4()).String() + hash, _ := crypto.HashFromString(input.TransactionHash) + iam := input.Amount.Mul(decimal.New(1, mixin.ValuePrecision)) + if !iam.IsInteger() { + panic(input.Amount.String()) + } + extra := []byte{SafeChainMixinKernel} + extra = append(extra, uuid.Must(uuid.FromString(SafeMixinKernelAssetId)).Bytes()...) + extra = append(extra, hash[:]...) + extra = binary.BigEndian.AppendUint64(extra, uint64(input.Index)) + extra = append(extra, iam.BigInt().Bytes()...) + + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + + out := testBuildObserverRequest(node, id, holder, common.ActionObserverHolderDeposit, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + + mainInputs, err := node.store.ListAllMixinKernelUTXOsForHolderAndAsset(ctx, holder, SafeMixinKernelAssetId) + require.Nil(err) + require.Len(mainInputs, t) + utxo := mainInputs[t-1] + require.Equal(uint32(input.Index), utxo.Index) + require.Equal(input.Amount, utxo.Amount) + require.Equal(hash.String(), utxo.TransactionHash) +} + +func testMixinKernelProposeAccount(ctx context.Context, require *require.Assertions, node *Node, signer, observer string) (string, string) { + id := uuid.Must(uuid.NewV4()).String() + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + extra := testRecipient() + price := decimal.NewFromFloat(testAccountPriceAmount) + out := testBuildHolderRequest(node, id, holder, common.ActionMixinKernelSafeProposeAccount, testAccountPriceAssetId, extra, price) + testStep(ctx, require, node, out) + b := testReadObserverResponse(ctx, require, node, id, common.ActionMixinKernelSafeProposeAccount) + wsa, err := mixin.ParseAddress(string(b)) + require.Nil(err) + require.Equal(testMixinKernelAddress, wsa.String()) + + safe, err := node.store.ReadSafeProposal(ctx, id) + require.Nil(err) + require.Equal(id, safe.RequestId) + require.Equal(holder, safe.Holder) + require.Equal(signer, safe.Signer) + require.Equal(observer, safe.Observer) + public := mixin.BuildAddress(holder, signer, observer) + require.Equal(testMixinKernelAddress, public.String()) + require.Equal(public.String(), safe.Address) + require.Equal(byte(1), safe.Threshold) + require.Len(safe.Receivers, 1) + require.Equal(testSafeBondReceiverId, safe.Receivers[0]) + + return id, wsa.String() +} + +func testMixinKernelApproveAccount(ctx context.Context, require *require.Assertions, node *Node, signer, observer string, rid, publicKey string) { + id := uuid.Must(uuid.NewV4()).String() + holder := testMixinKernelPublicKey(testMixinKernelHolderPrivate) + ms := fmt.Sprintf("APPROVE:%s:%s", rid, publicKey) + hash := mixin.HashMessageForSignature(ms) + hp, _ := crypto.KeyFromString(testMixinKernelHolderPrivate) + signature := hp.Sign(hash) + extra := uuid.FromStringOrNil(rid).Bytes() + extra = append(extra, signature[:]...) + out := testBuildObserverRequest(node, id, holder, common.ActionMixinKernelSafeApproveAccount, extra, common.CurveEdwards25519Mixin) + testStep(ctx, require, node, out) + b := testReadObserverResponse(ctx, require, node, id, common.ActionMixinKernelSafeApproveAccount) + wsa, err := mixin.ParseAddress(string(b)) + require.Nil(err) + require.Equal(testMixinKernelAddress, wsa.String()) + + safe, err := node.store.ReadSafe(ctx, holder) + require.Nil(err) + require.Equal(id, safe.RequestId) + require.Equal(holder, safe.Holder) + require.Equal(signer, safe.Signer) + require.Equal(observer, safe.Observer) + public := mixin.BuildAddress(holder, signer, observer) + require.Equal(testMixinKernelAddress, public.String()) + require.Equal(public.String(), safe.Address) + require.Equal(byte(1), safe.Threshold) + require.Len(safe.Receivers, 1) + require.Equal(testSafeBondReceiverId, safe.Receivers[0]) + var view crypto.Key + copy(view[:], safe.Extra) + require.Equal(view, public.PrivateViewKey) +} + +func testMixinKernelPublicKey(priv string) string { + key, _ := crypto.KeyFromString(priv) + return key.Public().String() +} diff --git a/keeper/network.go b/keeper/network.go index fcac6a17..3c92d21e 100644 --- a/keeper/network.go +++ b/keeper/network.go @@ -57,6 +57,9 @@ func (node *Node) writeNetworkInfo(ctx context.Context, req *common.Request) err } switch info.Chain { case SafeChainBitcoin, SafeChainLitecoin: + if info.Chain != SafeCurveChain(req.Curve) { + panic(req.Id) + } info.Hash = hex.EncodeToString(extra[17:]) valid, err := node.verifyBitcoinNetworkInfo(ctx, info) if err != nil { @@ -64,6 +67,20 @@ func (node *Node) writeNetworkInfo(ctx context.Context, req *common.Request) err } else if !valid { return node.store.FailRequest(ctx, req.Id) } + case SafeChainMixinKernel: + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Id) + } + info.Hash = hex.EncodeToString(extra[17:]) + hash, _ := crypto.HashFromString(info.Hash) + snap, err := common.ReadKernelSnapshot(node.conf.MixinRPC, hash) + if err != nil { + return fmt.Errorf("common.ReadKernelSnapshot(%v) => %v", info, err) + } else if snap.TopologicalOrder > info.Height && snap.TopologicalOrder-info.Height > 10000 { + panic(info.Hash) + } else if snap.TopologicalOrder < info.Height && info.Height-snap.TopologicalOrder > 10000 { + panic(info.Hash) + } case SafeChainEthereum, SafeChainMVM, SafeChainPolygon: info.Hash = "0x" + hex.EncodeToString(extra[17:]) valid, err := node.verifyEthereumNetworkInfo(ctx, info) @@ -94,8 +111,14 @@ func (node *Node) writeOperationParams(ctx context.Context, req *common.Request) panic(req.Id) } switch chain { - case SafeChainBitcoin: - case SafeChainLitecoin: + case SafeChainBitcoin, SafeChainLitecoin: + if chain != SafeCurveChain(req.Curve) { + panic(req.Id) + } + case SafeChainMixinKernel: + if req.Curve != common.CurveEdwards25519Mixin { + panic(req.Id) + } case SafeChainEthereum: case SafeChainMVM: case SafeChainPolygon: diff --git a/keeper/signer.go b/keeper/signer.go index 969924c3..d378e161 100644 --- a/keeper/signer.go +++ b/keeper/signer.go @@ -25,6 +25,7 @@ func (node *Node) sendSignerKeygenRequest(ctx context.Context, req *common.Reque crv := common.NormalizeCurve(req.Curve) switch crv { case common.CurveSecp256k1ECDSABitcoin: + case common.CurveEdwards25519Mixin: case common.CurveSecp256k1ECDSAEthereum: default: return node.store.FailRequest(ctx, req.Id) @@ -54,6 +55,7 @@ func (node *Node) sendSignerSignRequest(ctx context.Context, req *store.Signatur crv := common.NormalizeCurve(req.Curve) switch crv { case common.CurveSecp256k1ECDSABitcoin: + case common.CurveEdwards25519Mixin: case common.CurveSecp256k1ECDSAEthereum: default: panic(req.Curve) diff --git a/keeper/store/mixin.go b/keeper/store/mixin.go new file mode 100644 index 00000000..d1e9964f --- /dev/null +++ b/keeper/store/mixin.go @@ -0,0 +1,139 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/MixinNetwork/mixin/crypto" + "github.com/MixinNetwork/safe/apps/mixin" + "github.com/MixinNetwork/safe/common" + "github.com/shopspring/decimal" +) + +func (s *SQLite3Store) WriteMixinKernelOutputFromRequest(ctx context.Context, receiver, assetId string, utxo *mixin.Input, req *common.Request) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + cols := []string{"transaction_hash", "output_index", "address", "asset_id", "amount", "mask", "chain", "state", "spent_by", "request_id", "created_at", "updated_at"} + vals := []any{utxo.TransactionHash, utxo.Index, receiver, assetId, utxo.Amount.String(), utxo.Mask.String(), mixin.ChainMixinKernel, common.RequestStateInitial, nil, req.Id, req.CreatedAt, req.CreatedAt} + err = s.execOne(ctx, tx, buildInsertionSQL("mixin_outputs", cols), vals...) + if err != nil { + return fmt.Errorf("INSERT mixin_outputs %v", err) + } + err = s.execOne(ctx, tx, "UPDATE requests SET state=?, updated_at=? WHERE request_id=?", common.RequestStateDone, time.Now().UTC(), req.Id) + if err != nil { + return fmt.Errorf("UPDATE requests %v", err) + } + return tx.Commit() +} + +func (s *SQLite3Store) ReadMixinKernelUTXO(ctx context.Context, transactionHash string, index int) (*mixin.Input, string, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, "", err + } + defer tx.Rollback() + + return s.readMixinKernelUTXO(ctx, tx, transactionHash, index) +} + +func (s *SQLite3Store) ListAllMixinKernelUTXOsForHolderAndAsset(ctx context.Context, holder, assetId string) ([]*mixin.Input, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + safe, err := s.readSafe(ctx, tx, holder) + if err != nil { + return nil, err + } + + mainInputs, err := s.listAllMixinKernelUTXOsForAddressAndAsset(ctx, safe.Address, assetId, common.RequestStateInitial) + if err != nil { + return nil, err + } + + return mainInputs, nil +} + +func (s *SQLite3Store) ListPendingMixinKernelUTXOsForHolderAndAsset(ctx context.Context, holder, assetId string) ([]*mixin.Input, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + safe, err := s.readSafe(ctx, tx, holder) + if err != nil { + return nil, err + } + + mainInputs, err := s.listAllMixinKernelUTXOsForAddressAndAsset(ctx, safe.Address, assetId, common.RequestStatePending) + if err != nil { + return nil, err + } + + return mainInputs, nil +} + +func (s *SQLite3Store) listAllMixinKernelUTXOsForAddressAndAsset(ctx context.Context, receiver, assetId string, state int) ([]*mixin.Input, error) { + query := "SELECT transaction_hash,output_index,amount,asset_id,mask FROM mixin_outputs WHERE address=? AND asset_id=? AND state=? LIMIT 256" + rows, err := s.db.QueryContext(ctx, query, receiver, assetId, state) + if err != nil { + return nil, err + } + defer rows.Close() + + var inputs []*mixin.Input + for rows.Next() { + var input mixin.Input + var amount, asset, mask string + err := rows.Scan(&input.TransactionHash, &input.Index, &amount, &asset, &mask) + if err != nil { + return nil, err + } + input.Amount = decimal.RequireFromString(amount) + input.Asset = crypto.NewHash([]byte(asset)) + input.Mask, err = crypto.KeyFromString(mask) + if err != nil { + panic(err) + } + inputs = append(inputs, &input) + } + return inputs, nil +} + +func (s *SQLite3Store) readMixinKernelUTXO(ctx context.Context, tx *sql.Tx, transactionHash string, index int) (*mixin.Input, string, error) { + input := &mixin.Input{ + TransactionHash: transactionHash, + Index: uint32(index), + } + + query := "SELECT amount,asset_id,mask,spent_by FROM mixin_outputs WHERE transaction_hash=? AND output_index=?" + row := tx.QueryRowContext(ctx, query, transactionHash, index) + + var spent sql.NullString + var amount, asset, mask string + err := row.Scan(&amount, &asset, &mask, &spent) + if err == sql.ErrNoRows { + return nil, "", nil + } else if err != nil { + return nil, "", err + } + input.Amount = decimal.RequireFromString(amount) + input.Asset = crypto.NewHash([]byte(asset)) + input.Mask, err = crypto.KeyFromString(mask) + if err != nil { + panic(err) + } + return input, spent.String, nil +} diff --git a/keeper/store/schema.sql b/keeper/store/schema.sql index 2afa53ef..0d824082 100644 --- a/keeper/store/schema.sql +++ b/keeper/store/schema.sql @@ -167,6 +167,26 @@ CREATE INDEX IF NOT EXISTS bitcoin_outputs_by_address_state_created ON bitcoin_o +CREATE TABLE IF NOT EXISTS mixin_outputs ( + transaction_hash VARCHAR NOT NULL, + output_index INTEGER NOT NULL, + address VARCHAR NOT NULL, + asset_id VARCHAR NOT NULL, + amount VARCHAR NOT NULL, + mask VARCHAR NOT NULL, + chain INTEGER NOT NULL, + state INTEGER NOT NULL, + spent_by VARCHAR, + request_id VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY ('transaction_hash', 'output_index') +); + +CREATE UNIQUE INDEX IF NOT EXISTS mixin_outputs_by_request_id ON mixin_outputs(request_id); + + + CREATE TABLE IF NOT EXISTS ethereum_balances ( address VARCHAR NOT NULL, diff --git a/keeper/store/signature.go b/keeper/store/signature.go index 91766370..eb747a0a 100644 --- a/keeper/store/signature.go +++ b/keeper/store/signature.go @@ -143,11 +143,11 @@ func (s *SQLite3Store) FinishTransactionSignaturesWithRequest(ctx context.Contex return fmt.Errorf("UPDATE transactions %v", err) } - if transactionHasOutputs(safe.Chain) { - update := "UPDATE bitcoin_outputs SET state=?, updated_at=? WHERE spent_by=?" + if table := transactionInputTable(safe.Chain); table != "" { + update := fmt.Sprintf("UPDATE %s SET state=?, updated_at=? WHERE spent_by=?", table) err = s.execMultiple(ctx, tx, num, update, common.RequestStateDone, req.CreatedAt, transactionHash) if err != nil { - return fmt.Errorf("UPDATE bitcoin_outputs %v", err) + return fmt.Errorf("UPDATE %s %v", table, err) } } diff --git a/keeper/store/transaction.go b/keeper/store/transaction.go index 426eba6f..74a309cc 100644 --- a/keeper/store/transaction.go +++ b/keeper/store/transaction.go @@ -9,6 +9,7 @@ import ( "github.com/MixinNetwork/safe/apps/bitcoin" "github.com/MixinNetwork/safe/apps/ethereum" + "github.com/MixinNetwork/safe/apps/mixin" "github.com/MixinNetwork/safe/common" ) @@ -56,6 +57,14 @@ func TransactionInputsFromRawTransaction(trx *Transaction) []*TransactionInput { Index: pop.Index, }) } + case mixin.ChainMixinKernel: + ver, _ := mixin.ParsePartiallySignedTransaction(b) + for _, in := range ver.Inputs { + inputs = append(inputs, &TransactionInput{ + Hash: in.Hash.String(), + Index: uint32(in.Index), + }) + } default: panic(trx.Chain) } @@ -211,14 +220,15 @@ func (s *SQLite3Store) writeTransactionWithRequest(ctx context.Context, tx *sql. if err != nil { return fmt.Errorf("UPDATE requests %v", err) } - if !transactionHasOutputs(trx.Chain) { + table := transactionInputTable(trx.Chain) + if table == "" { return nil } - query := "UPDATE bitcoin_outputs SET state=?, spent_by=?, updated_at=? WHERE transaction_hash=? AND output_index=?" + query := fmt.Sprintf("UPDATE %s SET state=?, spent_by=?, updated_at=? WHERE transaction_hash=? AND output_index=?", table) for _, utxo := range utxos { err = s.execOne(ctx, tx, query, utxoState, trx.TransactionHash, trx.UpdatedAt, utxo.Hash, utxo.Index) if err != nil { - return fmt.Errorf("UPDATE bitcoin_outputs %v", err) + return fmt.Errorf("UPDATE %s %v", table, err) } } return nil @@ -234,14 +244,14 @@ func (s *SQLite3Store) RevokeTransactionWithRequest(ctx context.Context, trx *Tr } defer tx.Rollback() - if transactionHasOutputs(trx.Chain) { + if table := transactionInputTable(trx.Chain); table != "" { inputs := TransactionInputsFromRawTransaction(trx) - update := "UPDATE bitcoin_outputs SET state=?, spent_by=?, updated_at=? WHERE transaction_hash=? AND output_index=? AND spent_by=?" - query := "SELECT address FROM bitcoin_outputs WHERE transaction_hash=? AND output_index=?" + update := fmt.Sprintf("UPDATE %s SET state=?, spent_by=?, updated_at=? WHERE transaction_hash=? AND output_index=? AND spent_by=?", table) + query := fmt.Sprintf("SELECT address FROM %s WHERE transaction_hash=? AND output_index=?", table) for _, in := range inputs { err = s.execOne(ctx, tx, update, common.RequestStateInitial, nil, req.CreatedAt, in.Hash, in.Index, trx.TransactionHash) if err != nil { - return fmt.Errorf("UPDATE bitcoin_outputs %v", err) + return fmt.Errorf("UPDATE %s %v", table, err) } var receiver string @@ -314,12 +324,14 @@ func (s *SQLite3Store) readTransaction(ctx context.Context, tx *sql.Tx, transact return &trx, err } -func transactionHasOutputs(chain byte) bool { +func transactionInputTable(chain byte) string { switch chain { case bitcoin.ChainBitcoin, bitcoin.ChainLitecoin: - return true + return "bitcoin_outputs" + case mixin.ChainMixinKernel: + return "mixin_outputs" case ethereum.ChainEthereum, ethereum.ChainMVM, ethereum.ChainPolygon: - return false + return "" default: panic(chain) } diff --git a/observer/keeper.go b/observer/keeper.go index 16e5e46f..18210b51 100644 --- a/observer/keeper.go +++ b/observer/keeper.go @@ -51,6 +51,8 @@ func (node *Node) sendKeeperResponseWithReferences(ctx context.Context, holder s case keeper.SafeChainBitcoin: case keeper.SafeChainLitecoin: crv = common.CurveSecp256k1ECDSALitecoin + case keeper.SafeChainMixinKernel: + crv = common.CurveEdwards25519Mixin case keeper.SafeChainEthereum: crv = common.CurveSecp256k1ECDSAEthereum case keeper.SafeChainMVM: