Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

keeper adds mixin kernel as an app #14

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
83 changes: 83 additions & 0 deletions apps/mixin/account.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
22 changes: 22 additions & 0 deletions apps/mixin/common.go
Original file line number Diff line number Diff line change
@@ -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())
}
99 changes: 99 additions & 0 deletions apps/mixin/transaction.go
Original file line number Diff line number Diff line change
@@ -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)
}
22 changes: 22 additions & 0 deletions common/mixin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}

Expand Down
15 changes: 10 additions & 5 deletions common/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
5 changes: 5 additions & 0 deletions keeper/bitcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
29 changes: 18 additions & 11 deletions keeper/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,27 @@ 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"
"github.com/shopspring/decimal"
)

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
Expand All @@ -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}
}
Expand Down Expand Up @@ -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")
Expand Down
Loading
Loading