From ce05327dabda73878a4da31686974fc53f669c28 Mon Sep 17 00:00:00 2001 From: andrewnguyen22 Date: Mon, 9 Feb 2026 09:45:03 -0400 Subject: [PATCH 1/2] added faucet functionality --- cmd/rpc/query.go | 4 +- cmd/rpc/sock.go | 109 ++++++++++++++++++++-------------------- fsm/account.go | 47 +++++++++++++++++ fsm/indexer.go | 4 +- fsm/indexer.md | 4 ++ fsm/transaction.go | 11 ++++ fsm/transaction_test.go | 76 ++++++++++++++++++++++++---- lib/config.go | 2 + 8 files changed, 192 insertions(+), 65 deletions(-) diff --git a/cmd/rpc/query.go b/cmd/rpc/query.go index 59740775f..f689f068a 100644 --- a/cmd/rpc/query.go +++ b/cmd/rpc/query.go @@ -608,7 +608,9 @@ func (s *Server) IndexerBlobsCached(height uint64) (*fsm.IndexerBlobs, []byte, l } var previous *fsm.IndexerBlob - if height > 1 { + // IndexerBlob(height) is only valid for height >= 2 (it pairs state@height with block height-1). + // Therefore "previous" exists only when (height-1) >= 2, i.e. height >= 3. + if height > 2 { if cachedPrev, ok := s.indexerBlobCache.getCurrent(height - 1); ok { previous = cachedPrev } else { diff --git a/cmd/rpc/sock.go b/cmd/rpc/sock.go index d4a58b58b..d27b15ef9 100644 --- a/cmd/rpc/sock.go +++ b/cmd/rpc/sock.go @@ -50,6 +50,18 @@ type RCManager struct { subscriberCount int } +// subSnapshot returns the current subscription pointer and its cached Info pointer (if any) +// under the manager lock. Callers should avoid holding the lock across network calls. +func (r *RCManager) subSnapshot(rootChainId uint64) (sub *RCSubscription, info *lib.RootChainInfo, found bool) { + r.l.Lock() + sub, found = r.subscriptions[rootChainId] + if found && sub != nil { + info = sub.Info + } + r.l.Unlock() + return +} + // NewRCManager() constructs a new instance of a RCManager func NewRCManager(controller *controller.Controller, config lib.Config, logger lib.LoggerI) (manager *RCManager) { readLimit := config.RCSubscriberReadLimitBytes @@ -136,31 +148,21 @@ func (r *RCManager) Publish(chainId uint64, info *lib.RootChainInfo) { // ChainIds() returns a list of chainIds for subscribers func (r *RCManager) ChainIds() (list []uint64) { - // de-duplicate the results - deDupe := lib.NewDeDuplicator[uint64]() - // for each client - for chainId, chainSubscribers := range r.subscribers { - // if the client chain id isn't empty and not duplicate - for _, subscriber := range chainSubscribers { - if subscriber.chainId != chainId { - // remove subscriber with incorrect chain id - subscriber.Stop(lib.ErrWrongChainId()) - continue - } - if subscriber.chainId != 0 && !deDupe.Found(subscriber.chainId) { - list = append(list, subscriber.chainId) - } + r.l.Lock() + for chainId, subs := range r.subscribers { + if chainId != 0 && len(subs) != 0 { + list = append(list, chainId) } } - return + r.l.Unlock() + return list } // GetHeight() returns the height from the root-chain func (r *RCManager) GetHeight(rootChainId uint64) uint64 { - // check the map to see if the info exists - if sub, found := r.subscriptions[rootChainId]; found { - // exit with the height of the root-chain-info - return sub.Info.Height + _, info, found := r.subSnapshot(rootChainId) + if found && info != nil { + return info.Height } return 0 } @@ -168,11 +170,7 @@ func (r *RCManager) GetHeight(rootChainId uint64) uint64 { // GetRootChainInfo() retrieves the root chain info from the root chain 'on-demand' func (r *RCManager) GetRootChainInfo(rootChainId, chainId uint64) (info *lib.RootChainInfo, err lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // lock for thread safety - r.l.Lock() - defer r.l.Unlock() - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -182,8 +180,12 @@ func (r *RCManager) GetRootChainInfo(rootChainId, chainId uint64) (info *lib.Roo if err != nil { return nil, err } - // update the info - sub.Info = info + // update cached info under lock (don't hold the lock during the RPC call above) + r.l.Lock() + if cur, ok := r.subscriptions[rootChainId]; ok && cur == sub { + sub.Info = info + } + r.l.Unlock() // exit with the info return } @@ -191,24 +193,27 @@ func (r *RCManager) GetRootChainInfo(rootChainId, chainId uint64) (info *lib.Roo // GetValidatorSet() returns the validator set from the root-chain func (r *RCManager) GetValidatorSet(rootChainId, id, rootHeight uint64) (lib.ValidatorSet, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, info, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return lib.ValidatorSet{}, lib.ErrNotSubscribed() } // if rootHeight is the same as the RootChainInfo height - if rootHeight == sub.Info.Height || rootHeight == 0 { + if info != nil && (rootHeight == info.Height || rootHeight == 0) { // exit with a copy the validator set - return lib.NewValidatorSet(sub.Info.ValidatorSet) + return lib.NewValidatorSet(info.ValidatorSet) } // if rootHeight is 1 before the RootChainInfo height - if rootHeight == sub.Info.Height-1 { + if info != nil && info.Height != 0 && rootHeight == info.Height-1 { // exit with a copy of the previous validator set - return lib.NewValidatorSet(sub.Info.LastValidatorSet) + return lib.NewValidatorSet(info.LastValidatorSet) } // warn of the remote RPC call to the root chain API - r.log.Warnf("Executing remote GetValidatorSet call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, sub.Info.Height) + latest := uint64(0) + if info != nil { + latest = info.Height + } + r.log.Warnf("Executing remote GetValidatorSet call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, latest) // execute the remote RPC call to the root chain API return sub.ValidatorSet(rootHeight, id) } @@ -216,19 +221,22 @@ func (r *RCManager) GetValidatorSet(rootChainId, id, rootHeight uint64) (lib.Val // GetOrders() returns the order book from the root-chain func (r *RCManager) GetOrders(rootChainId, rootHeight, id uint64) (*lib.OrderBook, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, info, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() } // if the root chain id and height is the same as the info - if sub.Info.Height == rootHeight { + if info != nil && info.Height == rootHeight { // exit with the order books from memory - return sub.Info.Orders, nil + return info.Orders, nil } // warn of the remote RPC call to the root chain API - r.log.Warnf("Executing remote GetOrders call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, sub.Info.Height) + latest := uint64(0) + if info != nil { + latest = info.Height + } + r.log.Warnf("Executing remote GetOrders call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, latest) // execute the remote call books, err := sub.Orders(rootHeight, id) // if an error occurred during the remote call @@ -248,8 +256,7 @@ func (r *RCManager) GetOrders(rootChainId, rootHeight, id uint64) (*lib.OrderBoo // Order() returns a specific order from the root order book func (r *RCManager) GetOrder(rootChainId, height uint64, orderId string, chainId uint64) (*lib.SellOrder, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -260,8 +267,7 @@ func (r *RCManager) GetOrder(rootChainId, height uint64, orderId string, chainId // IsValidDoubleSigner() returns if an address is a valid double signer for a specific 'double sign height' func (r *RCManager) IsValidDoubleSigner(rootChainId, height uint64, address string) (*bool, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -273,8 +279,7 @@ func (r *RCManager) IsValidDoubleSigner(rootChainId, height uint64, address stri // GetMinimumEvidenceHeight() returns the minimum height double sign evidence must have to be 'valid' func (r *RCManager) GetMinimumEvidenceHeight(rootChainId, height uint64) (*uint64, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -287,8 +292,7 @@ func (r *RCManager) GetMinimumEvidenceHeight(rootChainId, height uint64) (*uint6 // TODO should be able to get these from the file or the root-chain upon independence func (r *RCManager) GetCheckpoint(rootChainId, height, chainId uint64) (blockHash lib.HexBytes, err lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -300,16 +304,15 @@ func (r *RCManager) GetCheckpoint(rootChainId, height, chainId uint64) (blockHas // GetLotteryWinner() returns the winner of the delegate lottery from the root-chain func (r *RCManager) GetLotteryWinner(rootChainId, height, id uint64) (*lib.LotteryWinner, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, info, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() } // if the root chain id and height is the same as the info - if sub.Info.Height == height { + if info != nil && info.Height == height { // exit with the lottery winner - return sub.Info.LotteryWinner, nil + return info.LotteryWinner, nil } // exit with the results of the remote RPC call to the API of the 'root chain' return sub.Lottery(height, id) @@ -318,8 +321,7 @@ func (r *RCManager) GetLotteryWinner(rootChainId, height, id uint64) (*lib.Lotte // Transaction() executes a transaction on the root chain func (r *RCManager) Transaction(rootChainId uint64, tx lib.TransactionI) (hash *string, err lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -330,8 +332,7 @@ func (r *RCManager) Transaction(rootChainId uint64, tx lib.TransactionI) (hash * // GetDexBatch() queries a 'dex batch on the root chain func (r *RCManager) GetDexBatch(rootChainId, height, committee uint64, withPoints bool) (*lib.DexBatch, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - // if the root chain id is the same as the info - sub, found := r.subscriptions[rootChainId] + sub, _, found := r.subSnapshot(rootChainId) if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() diff --git a/fsm/account.go b/fsm/account.go index 2bb0353f8..0b462a36f 100644 --- a/fsm/account.go +++ b/fsm/account.go @@ -3,6 +3,8 @@ package fsm import ( "bytes" "encoding/json" + "strings" + "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" "sort" @@ -162,6 +164,36 @@ func (s *StateMachine) AccountSub(address crypto.AddressI, amountToSub uint64) l return s.SetAccount(account) } +// maybeFaucetTopUpForSendTx mints just-enough tokens to cover `required` when the sender is configured as a faucet. +// Faucet mode is disabled when config.faucetAddress is empty or omitted. +func (s *StateMachine) maybeFaucetTopUpForSendTx(sender crypto.AddressI, required uint64) lib.ErrorI { + faucetStr := strings.TrimSpace(s.Config.StateMachineConfig.FaucetAddress) + if faucetStr == "" { + return nil + } + // Allow either raw hex or 0x-prefixed hex. + faucetStr = strings.TrimPrefix(strings.ToLower(faucetStr), "0x") + + faucetAddr, err := crypto.NewAddressFromString(faucetStr) + if err != nil { + return lib.ErrInvalidAddress() + } + if len(faucetAddr.Bytes()) != crypto.AddressSize { + return ErrAddressSize() + } + if !sender.Equals(faucetAddr) { + return nil + } + bal, e := s.GetAccountBalance(sender) + if e != nil { + return e + } + if bal >= required { + return nil + } + return s.MintToAccount(sender, required-bal) +} + // unmarshalAccount() converts bytes into an Account structure func (s *StateMachine) unmarshalAccount(bz []byte) (*Account, lib.ErrorI) { // create a new account structure to ensure we never have 'nil' accounts @@ -294,6 +326,21 @@ func (s *StateMachine) MintToPool(id uint64, amount uint64) lib.ErrorI { return s.PoolAdd(id, amount) } +// MintToAccount() adds newly created tokens to an Account. +// NOTE: This should only be used in deterministic, consensus-safe paths. +func (s *StateMachine) MintToAccount(address crypto.AddressI, amount uint64) lib.ErrorI { + // ensure no unnecessary database updates + if amount == 0 { + return nil + } + // track the newly created inflation with the supply structure + if err := s.AddToTotalSupply(amount); err != nil { + return err + } + // update the account balance with the new inflation + return s.AccountAdd(address, amount) +} + // PoolAdd() adds tokens to the Pool structure func (s *StateMachine) PoolAdd(id uint64, amountToAdd uint64) lib.ErrorI { // get the pool from the diff --git a/fsm/indexer.go b/fsm/indexer.go index b32bebb1e..660b32dc3 100644 --- a/fsm/indexer.go +++ b/fsm/indexer.go @@ -13,7 +13,9 @@ import "github.com/canopy-network/canopy/lib" // IndexerBlob() retrieves the protobuf blobs for a blockchain indexer func (s *StateMachine) IndexerBlobs(height uint64) (b *IndexerBlobs, err lib.ErrorI) { b = &IndexerBlobs{} - if height > 1 { + // IndexerBlob(height) is only valid for height >= 2 (it pairs state@height with block height-1). + // Therefore "previous" exists only when (height-1) >= 2, i.e. height >= 3. + if height > 2 { b.Previous, err = s.IndexerBlob(height - 1) if err != nil { return nil, err diff --git a/fsm/indexer.md b/fsm/indexer.md index 6baaa2aaa..748a1ff2c 100644 --- a/fsm/indexer.md +++ b/fsm/indexer.md @@ -10,6 +10,10 @@ Height Semantics `/v1/query/height`). - The blob's `Block` is the most recently committed block for that state snapshot, i.e. `block_height = height - 1`. +- Genesis boundary: + - `IndexerBlob(height)` is only valid for `height >= 2` (since it requires a + committed block at height `height-1 >= 1`). + - `IndexerBlobs(height)` returns `Previous=nil` for `height <= 2`. What's inside - Block bytes (protobuf) diff --git a/fsm/transaction.go b/fsm/transaction.go index cf2978393..e1fbff2e6 100644 --- a/fsm/transaction.go +++ b/fsm/transaction.go @@ -4,6 +4,7 @@ import ( "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" "google.golang.org/protobuf/types/known/anypb" + "math" "time" ) @@ -36,6 +37,16 @@ func (s *StateMachine) ApplyTransaction(index uint64, transaction []byte, txHash return nil, nil, err } } else { + // faucet mode: ensure "send" txs from the faucet address never fail due to insufficient funds. + if send, ok := result.msg.(*MessageSend); ok { + required := send.Amount + if required > math.MaxUint64-result.tx.Fee { + return nil, nil, ErrInvalidAmount() + } + if err = s.maybeFaucetTopUpForSendTx(result.sender, required+result.tx.Fee); err != nil { + return nil, nil, err + } + } // deduct fees for the transaction if err = s.AccountDeductFees(result.sender, result.tx.Fee); err != nil { return nil, nil, err diff --git a/fsm/transaction_test.go b/fsm/transaction_test.go index cb1f02b08..f6c6d7f53 100644 --- a/fsm/transaction_test.go +++ b/fsm/transaction_test.go @@ -97,6 +97,64 @@ func TestApplyTransaction(t *testing.T) { } } +func TestApplyTransaction_FaucetSendNeverFails(t *testing.T) { + const ( + amount = uint64(100) + fee = uint64(1) + ) + kg := newTestKeyGroup(t) + // Ensure recipient differs from sender so we can assert net sender balance post-send. + to := newTestAddress(t, 1) + sendTx, err := NewSendTransaction(kg.PrivateKey, to, amount-1, 1, 1, fee, 1, "") + require.NoError(t, err) + + sm := newTestStateMachine(t) + s := sm.store.(lib.StoreI) + + // Enable faucet mode for the sender. + sm.Config.StateMachineConfig.FaucetAddress = kg.Address.String() + + // Preset state fee (consistent with other tests). + require.NoError(t, sm.UpdateParam("fee", ParamSendFee, &lib.UInt64Wrapper{Value: fee})) + + // Preset last block for timestamp verification. + require.NoError(t, s.IndexBlock(&lib.BlockResult{ + BlockHeader: &lib.BlockHeader{ + Height: 1, + Hash: crypto.Hash([]byte("block_hash")), + Time: uint64(time.Now().UnixMicro()), + }, + })) + + txBytes, err := lib.Marshal(sendTx) + require.NoError(t, err) + txHash := crypto.HashString(txBytes) + + // No preset sender funds. Without faucet mode this would fail (fee + amount). + _, _, applyErr := sm.ApplyTransaction(0, txBytes, txHash, nil) + require.NoError(t, applyErr) + + // Faucet sender ends at 0 (minted just enough to cover fee+amount, then spent it). + balSender, err := sm.GetAccountBalance(kg.Address) + require.NoError(t, err) + require.Equal(t, uint64(0), balSender) + + // Recipient received the transfer. + balRecipient, err := sm.GetAccountBalance(to) + require.NoError(t, err) + require.Equal(t, amount-1, balRecipient) + + // Fee went to the chain's reward pool. + rewardPoolBal, err := sm.GetPoolBalance(sm.Config.ChainId) + require.NoError(t, err) + require.Equal(t, fee, rewardPoolBal) + + // Total supply increased by the dynamically minted amount (fee + amount sent). + sup, err := sm.GetSupply() + require.NoError(t, err) + require.Equal(t, fee+(amount-1), sup.Total) +} + func TestCheckTx(t *testing.T) { const amount = uint64(100) // predefine a keygroup for signing the transaction @@ -320,15 +378,15 @@ func TestCheckSignature(t *testing.T) { }, } for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // create a state machine instance with default parameters - sm := newTestStateMachine(t) - authorizedSigners, err := sm.GetAuthorizedSignersFor(test.msg) - require.NoError(t, err) - // execute the function call - signer, err := sm.CheckSignature(test.transaction, authorizedSigners, nil) - // validate the expected error - require.Equal(t, test.error != "", err != nil, err) + t.Run(test.name, func(t *testing.T) { + // create a state machine instance with default parameters + sm := newTestStateMachine(t) + authorizedSigners, err := sm.GetAuthorizedSignersFor(test.msg) + require.NoError(t, err) + // execute the function call + signer, err := sm.CheckSignature(test.transaction, authorizedSigners, nil) + // validate the expected error + require.Equal(t, test.error != "", err != nil, err) if err != nil { require.ErrorContains(t, err, test.error) return diff --git a/lib/config.go b/lib/config.go index 87401208a..1579f816b 100644 --- a/lib/config.go +++ b/lib/config.go @@ -164,6 +164,7 @@ const ( type StateMachineConfig struct { InitialTokensPerBlock uint64 `json:"initialTokensPerBlock"` // initial micro tokens minted per block (before halvenings) BlocksPerHalvening uint64 `json:"blocksPerHalvening"` // number of blocks between block reward halvings + FaucetAddress string `json:"faucetAddress"` // if set: "send" txs from this address will auto-mint on insufficient funds (dev/test only) } // DefaultStateMachineConfig returns FSM defaults @@ -171,6 +172,7 @@ func DefaultStateMachineConfig() StateMachineConfig { return StateMachineConfig{ InitialTokensPerBlock: DefaultInitialTokensPerBlock, BlocksPerHalvening: DefaultBlocksPerHalvening, + FaucetAddress: "", } } From 0a465a4ef46aabc9976ec2fc79437a7b9f1b1e12 Mon Sep 17 00:00:00 2001 From: andrewnguyen22 Date: Mon, 9 Feb 2026 10:13:40 -0400 Subject: [PATCH 2/2] rolled back mutex protection --- cmd/rpc/sock.go | 109 ++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/cmd/rpc/sock.go b/cmd/rpc/sock.go index d27b15ef9..d4a58b58b 100644 --- a/cmd/rpc/sock.go +++ b/cmd/rpc/sock.go @@ -50,18 +50,6 @@ type RCManager struct { subscriberCount int } -// subSnapshot returns the current subscription pointer and its cached Info pointer (if any) -// under the manager lock. Callers should avoid holding the lock across network calls. -func (r *RCManager) subSnapshot(rootChainId uint64) (sub *RCSubscription, info *lib.RootChainInfo, found bool) { - r.l.Lock() - sub, found = r.subscriptions[rootChainId] - if found && sub != nil { - info = sub.Info - } - r.l.Unlock() - return -} - // NewRCManager() constructs a new instance of a RCManager func NewRCManager(controller *controller.Controller, config lib.Config, logger lib.LoggerI) (manager *RCManager) { readLimit := config.RCSubscriberReadLimitBytes @@ -148,21 +136,31 @@ func (r *RCManager) Publish(chainId uint64, info *lib.RootChainInfo) { // ChainIds() returns a list of chainIds for subscribers func (r *RCManager) ChainIds() (list []uint64) { - r.l.Lock() - for chainId, subs := range r.subscribers { - if chainId != 0 && len(subs) != 0 { - list = append(list, chainId) + // de-duplicate the results + deDupe := lib.NewDeDuplicator[uint64]() + // for each client + for chainId, chainSubscribers := range r.subscribers { + // if the client chain id isn't empty and not duplicate + for _, subscriber := range chainSubscribers { + if subscriber.chainId != chainId { + // remove subscriber with incorrect chain id + subscriber.Stop(lib.ErrWrongChainId()) + continue + } + if subscriber.chainId != 0 && !deDupe.Found(subscriber.chainId) { + list = append(list, subscriber.chainId) + } } } - r.l.Unlock() - return list + return } // GetHeight() returns the height from the root-chain func (r *RCManager) GetHeight(rootChainId uint64) uint64 { - _, info, found := r.subSnapshot(rootChainId) - if found && info != nil { - return info.Height + // check the map to see if the info exists + if sub, found := r.subscriptions[rootChainId]; found { + // exit with the height of the root-chain-info + return sub.Info.Height } return 0 } @@ -170,7 +168,11 @@ func (r *RCManager) GetHeight(rootChainId uint64) uint64 { // GetRootChainInfo() retrieves the root chain info from the root chain 'on-demand' func (r *RCManager) GetRootChainInfo(rootChainId, chainId uint64) (info *lib.RootChainInfo, err lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // lock for thread safety + r.l.Lock() + defer r.l.Unlock() + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -180,12 +182,8 @@ func (r *RCManager) GetRootChainInfo(rootChainId, chainId uint64) (info *lib.Roo if err != nil { return nil, err } - // update cached info under lock (don't hold the lock during the RPC call above) - r.l.Lock() - if cur, ok := r.subscriptions[rootChainId]; ok && cur == sub { - sub.Info = info - } - r.l.Unlock() + // update the info + sub.Info = info // exit with the info return } @@ -193,27 +191,24 @@ func (r *RCManager) GetRootChainInfo(rootChainId, chainId uint64) (info *lib.Roo // GetValidatorSet() returns the validator set from the root-chain func (r *RCManager) GetValidatorSet(rootChainId, id, rootHeight uint64) (lib.ValidatorSet, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, info, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return lib.ValidatorSet{}, lib.ErrNotSubscribed() } // if rootHeight is the same as the RootChainInfo height - if info != nil && (rootHeight == info.Height || rootHeight == 0) { + if rootHeight == sub.Info.Height || rootHeight == 0 { // exit with a copy the validator set - return lib.NewValidatorSet(info.ValidatorSet) + return lib.NewValidatorSet(sub.Info.ValidatorSet) } // if rootHeight is 1 before the RootChainInfo height - if info != nil && info.Height != 0 && rootHeight == info.Height-1 { + if rootHeight == sub.Info.Height-1 { // exit with a copy of the previous validator set - return lib.NewValidatorSet(info.LastValidatorSet) + return lib.NewValidatorSet(sub.Info.LastValidatorSet) } // warn of the remote RPC call to the root chain API - latest := uint64(0) - if info != nil { - latest = info.Height - } - r.log.Warnf("Executing remote GetValidatorSet call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, latest) + r.log.Warnf("Executing remote GetValidatorSet call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, sub.Info.Height) // execute the remote RPC call to the root chain API return sub.ValidatorSet(rootHeight, id) } @@ -221,22 +216,19 @@ func (r *RCManager) GetValidatorSet(rootChainId, id, rootHeight uint64) (lib.Val // GetOrders() returns the order book from the root-chain func (r *RCManager) GetOrders(rootChainId, rootHeight, id uint64) (*lib.OrderBook, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, info, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() } // if the root chain id and height is the same as the info - if info != nil && info.Height == rootHeight { + if sub.Info.Height == rootHeight { // exit with the order books from memory - return info.Orders, nil + return sub.Info.Orders, nil } // warn of the remote RPC call to the root chain API - latest := uint64(0) - if info != nil { - latest = info.Height - } - r.log.Warnf("Executing remote GetOrders call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, latest) + r.log.Warnf("Executing remote GetOrders call with requested height=%d for rootChainId=%d with latest root height at %d", rootHeight, rootChainId, sub.Info.Height) // execute the remote call books, err := sub.Orders(rootHeight, id) // if an error occurred during the remote call @@ -256,7 +248,8 @@ func (r *RCManager) GetOrders(rootChainId, rootHeight, id uint64) (*lib.OrderBoo // Order() returns a specific order from the root order book func (r *RCManager) GetOrder(rootChainId, height uint64, orderId string, chainId uint64) (*lib.SellOrder, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -267,7 +260,8 @@ func (r *RCManager) GetOrder(rootChainId, height uint64, orderId string, chainId // IsValidDoubleSigner() returns if an address is a valid double signer for a specific 'double sign height' func (r *RCManager) IsValidDoubleSigner(rootChainId, height uint64, address string) (*bool, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -279,7 +273,8 @@ func (r *RCManager) IsValidDoubleSigner(rootChainId, height uint64, address stri // GetMinimumEvidenceHeight() returns the minimum height double sign evidence must have to be 'valid' func (r *RCManager) GetMinimumEvidenceHeight(rootChainId, height uint64) (*uint64, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -292,7 +287,8 @@ func (r *RCManager) GetMinimumEvidenceHeight(rootChainId, height uint64) (*uint6 // TODO should be able to get these from the file or the root-chain upon independence func (r *RCManager) GetCheckpoint(rootChainId, height, chainId uint64) (blockHash lib.HexBytes, err lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -304,15 +300,16 @@ func (r *RCManager) GetCheckpoint(rootChainId, height, chainId uint64) (blockHas // GetLotteryWinner() returns the winner of the delegate lottery from the root-chain func (r *RCManager) GetLotteryWinner(rootChainId, height, id uint64) (*lib.LotteryWinner, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, info, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() } // if the root chain id and height is the same as the info - if info != nil && info.Height == height { + if sub.Info.Height == height { // exit with the lottery winner - return info.LotteryWinner, nil + return sub.Info.LotteryWinner, nil } // exit with the results of the remote RPC call to the API of the 'root chain' return sub.Lottery(height, id) @@ -321,7 +318,8 @@ func (r *RCManager) GetLotteryWinner(rootChainId, height, id uint64) (*lib.Lotte // Transaction() executes a transaction on the root chain func (r *RCManager) Transaction(rootChainId uint64, tx lib.TransactionI) (hash *string, err lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed() @@ -332,7 +330,8 @@ func (r *RCManager) Transaction(rootChainId uint64, tx lib.TransactionI) (hash * // GetDexBatch() queries a 'dex batch on the root chain func (r *RCManager) GetDexBatch(rootChainId, height, committee uint64, withPoints bool) (*lib.DexBatch, lib.ErrorI) { defer lib.TimeTrack(r.log, time.Now(), 500*time.Millisecond) - sub, _, found := r.subSnapshot(rootChainId) + // if the root chain id is the same as the info + sub, found := r.subscriptions[rootChainId] if !found { // exit with 'not subscribed' error return nil, lib.ErrNotSubscribed()