Skip to content

Commit

Permalink
custodian snapshot validate timestamp and payee
Browse files Browse the repository at this point in the history
  • Loading branch information
tristenblakely committed Aug 5, 2023
1 parent 79620c1 commit 4e4bb61
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 75 deletions.
35 changes: 20 additions & 15 deletions common/custodian.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
type CustodianUpdateRequest struct {
Custodian *Address
Nodes []*CustodianNode
Signature *crypto.Signature
Transaction crypto.Hash
Timestamp uint64
}
Expand Down Expand Up @@ -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])
Expand All @@ -99,26 +100,26 @@ 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)
for i := range nodes {
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
uniqueKeys[cn.Custodian.PublicSpendKey] = true
uniqueKeys[cn.Custodian.PublicViewKey] = true
err = cn.validate()
if err != nil {
return nil, nil, nil, err
return nil, err
}
nodes[i] = cn
}
Expand All @@ -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 {
Expand All @@ -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())
Expand All @@ -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)
}

Expand All @@ -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)
}
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions common/custodian_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
67 changes: 67 additions & 0 deletions kernel/custodian.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions kernel/self.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
79 changes: 25 additions & 54 deletions storage/badger_custodian.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package storage

import (
"encoding/binary"
"encoding/hex"
"fmt"

"github.com/MixinNetwork/mixin/common"
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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 {
Expand Down

0 comments on commit 4e4bb61

Please sign in to comment.