diff --git a/common/custodian.go b/common/custodian.go index e92d1779b..5fee4f783 100644 --- a/common/custodian.go +++ b/common/custodian.go @@ -19,6 +19,7 @@ const ( type CustodianUpdateRequest struct { Custodian *Address Nodes []*CustodianNode + Signature *crypto.Signature Transaction crypto.Hash Timestamp uint64 } @@ -86,9 +87,9 @@ func ParseCustodianNode(extra []byte) (*CustodianNode, error) { return &cn, nil } -func ParseCustodianUpdateNodesExtra(extra []byte) (*Address, []*CustodianNode, *crypto.Signature, error) { +func ParseCustodianUpdateNodesExtra(extra []byte) (*CustodianUpdateRequest, error) { if len(extra) < 64+custodianNodeExtraSize*custodianNodesMinimumCount+64 { - return nil, nil, nil, fmt.Errorf("invalid custodian update extra %x", extra) + return nil, fmt.Errorf("invalid custodian update extra %x", extra) } var custodian Address copy(custodian.PublicSpendKey[:], extra[:32]) @@ -99,7 +100,7 @@ func ParseCustodianUpdateNodesExtra(extra []byte) (*Address, []*CustodianNode, * // 1 || custodian (Address) || payee (Address) || node id (Hash) || signerSig || payeeSig || custodianSig nodesExtra := extra[64 : len(extra)-64] if len(nodesExtra)%custodianNodeExtraSize != 0 { - return nil, nil, nil, fmt.Errorf("invalid custodian update extra %x", extra) + return nil, fmt.Errorf("invalid custodian update extra %x", extra) } nodes := make([]*CustodianNode, len(nodesExtra)/custodianNodeExtraSize) uniqueKeys := make(map[crypto.Key]bool) @@ -107,10 +108,10 @@ func ParseCustodianUpdateNodesExtra(extra []byte) (*Address, []*CustodianNode, * cne := nodesExtra[i*custodianNodeExtraSize : (i+1)*custodianNodeExtraSize] cn, err := ParseCustodianNode(cne) if err != nil { - return nil, nil, nil, err + return nil, err } if uniqueKeys[cn.Payee.PublicSpendKey] || uniqueKeys[cn.Custodian.PublicSpendKey] { - return nil, nil, nil, fmt.Errorf("duplicate custodian or payee keys %x", cne) + return nil, fmt.Errorf("duplicate custodian or payee keys %x", cne) } uniqueKeys[cn.Payee.PublicSpendKey] = true uniqueKeys[cn.Payee.PublicViewKey] = true @@ -118,7 +119,7 @@ func ParseCustodianUpdateNodesExtra(extra []byte) (*Address, []*CustodianNode, * uniqueKeys[cn.Custodian.PublicViewKey] = true err = cn.validate() if err != nil { - return nil, nil, nil, err + return nil, err } nodes[i] = cn } @@ -131,10 +132,14 @@ func ParseCustodianUpdateNodesExtra(extra []byte) (*Address, []*CustodianNode, * sortedExtra = append(sortedExtra, n.Extra...) } if !bytes.Equal(nodesExtra, sortedExtra) { - return nil, nil, nil, fmt.Errorf("invalid custodian nodes extra sort order %x", extra) + return nil, fmt.Errorf("invalid custodian nodes extra sort order %x", extra) } - return &custodian, nodes, &prevCustodianSig, nil + return &CustodianUpdateRequest{ + Custodian: &custodian, + Nodes: nodes, + Signature: &prevCustodianSig, + }, nil } func (tx *Transaction) validateCustodianUpdateNodes(store CustodianReader) error { @@ -155,12 +160,12 @@ func (tx *Transaction) validateCustodianUpdateNodes(store CustodianReader) error return fmt.Errorf("invalid custodian update output receiver %v", out) } - custodian, custodianNodes, prevCustodianSig, err := ParseCustodianUpdateNodesExtra(tx.Extra) + curs, err := ParseCustodianUpdateNodesExtra(tx.Extra) if err != nil { return err } - if len(custodianNodes) < custodianNodesMinimumCount { - return fmt.Errorf("invalid custodian nodes count %d", len(custodianNodes)) + if len(curs.Nodes) < custodianNodesMinimumCount { + return fmt.Errorf("invalid custodian nodes count %d", len(curs.Nodes)) } now := uint64(time.Now().UnixNano()) @@ -175,7 +180,7 @@ func (tx *Transaction) validateCustodianUpdateNodes(store CustodianReader) error } prev = &CustodianUpdateRequest{Custodian: &domains[0].Account} } - if !prev.Custodian.PublicSpendKey.Verify(tx.Extra[:len(tx.Extra)-64], *prevCustodianSig) { + if !prev.Custodian.PublicSpendKey.Verify(tx.Extra[:len(tx.Extra)-64], *curs.Signature) { return fmt.Errorf("invalid custodian update approval signature %x", tx.Extra) } @@ -187,7 +192,7 @@ func (tx *Transaction) validateCustodianUpdateNodes(store CustodianReader) error panic(prev.Custodian.String()) } total, price := Zero, NewInteger(custodianNodePrice) - for _, n := range custodianNodes { + for _, n := range curs.Nodes { if filter[n.Custodian.String()] != n.Payee.String() { total = total.Add(price) } @@ -197,10 +202,10 @@ func (tx *Transaction) validateCustodianUpdateNodes(store CustodianReader) error return fmt.Errorf("invalid custodian nodes update price %v", out) } - if custodian.String() != prev.Custodian.String() { + if curs.Custodian.String() != prev.Custodian.String() { return nil } - if len(filter) != 0 || len(prev.Nodes) != len(custodianNodes) { + if len(filter) != 0 || len(prev.Nodes) != len(curs.Nodes) { return fmt.Errorf("custodian account and nodes mismatch %x", tx.Extra) } return nil diff --git a/common/custodian_test.go b/common/custodian_test.go index e06eefa87..4a5f27760 100644 --- a/common/custodian_test.go +++ b/common/custodian_test.go @@ -161,12 +161,12 @@ func (s *testCustodianStore) ReadCustodian(ts uint64) (*CustodianUpdateRequest, if s.custodianUpdateNodesExtra == nil { return nil, nil } - custodian, nodes, _, err := ParseCustodianUpdateNodesExtra(s.custodianUpdateNodesExtra) - return &CustodianUpdateRequest{ - Custodian: custodian, - Nodes: nodes, - Timestamp: s.custodianUpdateNodesTimestamp, - }, err + cur, err := ParseCustodianUpdateNodesExtra(s.custodianUpdateNodesExtra) + if err != nil { + return nil, err + } + cur.Timestamp = s.custodianUpdateNodesTimestamp + return cur, nil } func testBuildAddress(require *require.Assertions) Address { diff --git a/kernel/custodian.go b/kernel/custodian.go new file mode 100644 index 000000000..cea01574d --- /dev/null +++ b/kernel/custodian.go @@ -0,0 +1,67 @@ +package kernel + +import ( + "fmt" + + "github.com/MixinNetwork/mixin/common" + "github.com/MixinNetwork/mixin/config" + "github.com/MixinNetwork/mixin/kernel/internal/clock" +) + +func (node *Node) validateCustodianUpdateNodes(s *common.Snapshot, tx *common.VersionedTransaction, finalized bool) error { + timestamp := s.Timestamp + if s.Timestamp == 0 && s.NodeId == node.IdForNetwork { + timestamp = uint64(clock.Now().UnixNano()) + } + + if timestamp < node.Epoch { + return fmt.Errorf("invalid snapshot timestamp %d %d", node.Epoch, timestamp) + } + since := timestamp - node.Epoch + hours := int(since / 3600000000000) + kmb, kme := config.KernelMintTimeBegin, config.KernelMintTimeEnd + if hours%24+1 >= kmb && hours%24 <= kme+1 { + return fmt.Errorf("invalid custodian update hour %d", hours%24) + } + + threshold := config.SnapshotRoundGap * config.SnapshotReferenceThreshold + if !finalized && timestamp+threshold*2 < node.GraphTimestamp { + return fmt.Errorf("invalid custodian update snapshot timestamp %d %d", node.GraphTimestamp, timestamp) + } + + curs, err := common.ParseCustodianUpdateNodesExtra(tx.Extra) + if err != nil { + return err + } + if len(curs.Nodes) < 7 { + return fmt.Errorf("invalid custodian nodes count %d", len(curs.Nodes)) + } + + prev, err := node.persistStore.ReadCustodian(timestamp) + if err != nil { + return err + } + if prev == nil { + domains := node.persistStore.ReadDomains() + if len(domains) != 1 { + return fmt.Errorf("invalid domains count %d", len(domains)) + } + prev = &common.CustodianUpdateRequest{Custodian: &domains[0].Account} + } + if !prev.Custodian.PublicSpendKey.Verify(tx.Extra[:len(tx.Extra)-64], *curs.Signature) { + return fmt.Errorf("invalid custodian update approval signature %x", tx.Extra) + } + + all := node.persistStore.ReadAllNodes(timestamp, false) + filter := make(map[string]bool) + for _, n := range all { + filter[n.Payee.String()] = true + } + for _, n := range curs.Nodes { + if filter[n.Payee.String()] { + continue + } + return fmt.Errorf("invalid custodian node %v", n) + } + return nil +} diff --git a/kernel/self.go b/kernel/self.go index 3f6a0ab6d..eb73d9970 100644 --- a/kernel/self.go +++ b/kernel/self.go @@ -94,6 +94,14 @@ func (node *Node) validateKernelSnapshot(s *common.Snapshot, tx *common.Versione logger.Verbosef("validateNodeRemoveSnapshot ERROR %v %s %s\n", s, hex.EncodeToString(tx.PayloadMarshal()), err.Error()) return err } + case common.TransactionTypeCustodianUpdateNodes: + err := node.validateCustodianUpdateNodes(s, tx, finalized) + if err != nil { + logger.Verbosef("validateCustodianUpdateNodes ERROR %v %s %s\n", s, hex.EncodeToString(tx.PayloadMarshal()), err.Error()) + return err + } + case common.TransactionTypeCustodianSlashNodes: + return fmt.Errorf("not implemented %v", tx) } if s.NodeId != node.IdForNetwork && s.RoundNumber == 0 && tx.TransactionType() != common.TransactionTypeNodeAccept { return fmt.Errorf("invalid initial transaction type %d", tx.TransactionType()) diff --git a/storage/badger_custodian.go b/storage/badger_custodian.go index 34bbbd3f2..fa14aabf2 100644 --- a/storage/badger_custodian.go +++ b/storage/badger_custodian.go @@ -2,7 +2,6 @@ package storage import ( "encoding/binary" - "encoding/hex" "fmt" "github.com/MixinNetwork/mixin/common" @@ -25,7 +24,7 @@ func (s *BadgerStore) ListCustodianUpdates() ([]*common.CustodianUpdateRequest, var curs []*common.CustodianUpdateRequest it.Seek(graphCustodianUpdateKey(0)) for ; it.ValidForPrefix([]byte(graphPrefixCustodianUpdate)); it.Next() { - cur, err := parseCustodianUpdateItem(it) + cur, err := parseCustodianUpdateItem(txn, it) if err != nil { return nil, err } @@ -41,24 +40,6 @@ func (s *BadgerStore) ReadCustodian(ts uint64) (*common.CustodianUpdateRequest, return readCustodianAccount(txn, ts) } -func parseCustodianNodes(val []byte) ([]*common.CustodianNode, error) { - count := int(val[0]) - size := len(val[1:]) / count - if size*count != len(val)-1 { - panic(hex.EncodeToString(val)) - } - nodes := make([]*common.CustodianNode, count) - for i := 0; i < int(val[0]); i++ { - extra := val[1+i*size : 1+(i+1)*size] - node, err := common.ParseCustodianNode(extra) - if err != nil { - return nil, err - } - nodes[i] = node - } - return nodes, nil -} - func readCustodianAccount(txn *badger.Txn, ts uint64) (*common.CustodianUpdateRequest, error) { opts := badger.DefaultIteratorOptions opts.PrefetchValues = true @@ -70,73 +51,63 @@ func readCustodianAccount(txn *badger.Txn, ts uint64) (*common.CustodianUpdateRe it.Seek(graphCustodianUpdateKey(ts)) if it.ValidForPrefix([]byte(graphPrefixCustodianUpdate)) { - return parseCustodianUpdateItem(it) + return parseCustodianUpdateItem(txn, it) } return nil, nil } -func parseCustodianUpdateItem(it *badger.Iterator) (*common.CustodianUpdateRequest, error) { +func parseCustodianUpdateItem(txn *badger.Txn, it *badger.Iterator) (*common.CustodianUpdateRequest, error) { key := it.Item().KeyCopy(nil) ts := graphCustodianAccountTimestamp(key) val, err := it.Item().ValueCopy(nil) if err != nil { return nil, err } - if len(val) < 97 { + if len(val) != 32 { panic(len(val)) } - nodes, err := parseCustodianNodes(val[96:]) + var hash crypto.Hash + copy(hash[:], val) + tx, err := readTransaction(txn, hash) if err != nil { return nil, err } - - var hash crypto.Hash - copy(hash[:], val[:32]) - var account common.Address - copy(account.PublicSpendKey[:], val[32:64]) - copy(account.PublicViewKey[:], val[64:96]) - return &common.CustodianUpdateRequest{ - Custodian: &account, - Nodes: nodes, - Transaction: hash, - Timestamp: ts, - }, nil + cur, err := common.ParseCustodianUpdateNodesExtra(tx.Extra) + if err != nil { + return nil, err + } + cur.Transaction = hash + cur.Timestamp = ts + return cur, nil } func writeCustodianNodes(txn *badger.Txn, snapTime uint64, utxo *common.UTXOWithLock, extra []byte) error { - custodian, nodes, _, err := common.ParseCustodianUpdateNodesExtra(extra) + now, err := common.ParseCustodianUpdateNodesExtra(extra) if err != nil { panic(fmt.Errorf("common.ParseCustodianUpdateNodesExtra(%x) => %v", extra, err)) } - cur, err := readCustodianAccount(txn, snapTime) + if len(now.Nodes) > 50 { + panic(len(now.Nodes)) + } + prev, err := readCustodianAccount(txn, snapTime) if err != nil { return err } switch { - case cur == nil: - case cur.Timestamp > snapTime: + case prev == nil: + case prev.Timestamp > snapTime: panic(utxo.Hash.String()) - case cur.Timestamp == snapTime && custodian.String() == cur.Custodian.String(): + case prev.Timestamp == snapTime && now.Custodian.String() == prev.Custodian.String(): return nil - case cur.Timestamp == snapTime && custodian.String() != cur.Custodian.String(): + case prev.Timestamp == snapTime && now.Custodian.String() != prev.Custodian.String(): panic(utxo.Hash.String()) - case cur.Timestamp < snapTime: + case prev.Timestamp < snapTime: } key := graphCustodianUpdateKey(snapTime) - val := append(utxo.Hash[:], custodian.PublicSpendKey[:]...) - val = append(val, custodian.PublicViewKey[:]...) - if len(nodes) > 50 { - panic(len(nodes)) - } - val = append(val, byte(len(nodes))) - for _, n := range nodes { - val = append(val, n.Extra...) - } - - return txn.Set(key, val) + return txn.Set(key, utxo.Hash[:]) } func graphCustodianUpdateKey(ts uint64) []byte {