diff --git a/api/mod.go b/api/mod.go index e4a8f769..31adb4d2 100644 --- a/api/mod.go +++ b/api/mod.go @@ -220,7 +220,7 @@ func (g *Gateway) sendTransaction(ctx *fasthttp.RequestCtx) { tx := wavelet.AttachSenderToTransaction( g.keys, - wavelet.Transaction{Tag: sys.Tag(req.Tag), Payload: req.payload, Creator: req.creator, CreatorSignature: req.signature}, + &wavelet.Transaction{Tag: sys.Tag(req.Tag), Payload: req.payload, Creator: req.creator, CreatorSignature: req.signature}, g.ledger.Graph().FindEligibleParents()..., ) @@ -231,7 +231,7 @@ func (g *Gateway) sendTransaction(ctx *fasthttp.RequestCtx) { return } - g.render(ctx, &sendTransactionResponse{ledger: g.ledger, tx: &tx}) + g.render(ctx, &sendTransactionResponse{ledger: g.ledger, tx: tx}) } func (g *Gateway) ledgerStatus(ctx *fasthttp.RequestCtx) { diff --git a/cmd/wavelet/shell.go b/cmd/wavelet/shell.go index bc8575e3..f49240d4 100644 --- a/cmd/wavelet/shell.go +++ b/cmd/wavelet/shell.go @@ -591,7 +591,7 @@ func (cli *CLI) withdrawReward(cmd []string) { Msgf("Success! Your reward withdrawal transaction ID: %x", tx.ID) } -func (cli *CLI) sendTransaction(tx wavelet.Transaction) (wavelet.Transaction, error) { +func (cli *CLI) sendTransaction(tx *wavelet.Transaction) (*wavelet.Transaction, error) { tx = wavelet.AttachSenderToTransaction(cli.keys, tx, cli.ledger.Graph().FindEligibleParents()...) if err := cli.ledger.AddTransaction(tx); err != nil && errors.Cause(err) != wavelet.ErrMissingParents { diff --git a/collapse.go b/collapse.go new file mode 100644 index 00000000..045db43c --- /dev/null +++ b/collapse.go @@ -0,0 +1,248 @@ +package wavelet + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "github.com/perlin-network/wavelet/avl" + "github.com/perlin-network/wavelet/log" + "github.com/perlin-network/wavelet/sys" + queue2 "github.com/phf/go-queue/queue" + "github.com/pkg/errors" + "golang.org/x/crypto/blake2b" +) + +func processRewardWithdrawals(round uint64, snapshot *avl.Tree) { + rws := GetRewardWithdrawalRequests(snapshot, round-uint64(sys.RewardWithdrawalsRoundLimit)) + + for _, rw := range rws { + balance, _ := ReadAccountBalance(snapshot, rw.account) + WriteAccountBalance(snapshot, rw.account, balance+rw.amount) + + snapshot.Delete(rw.Key()) + } +} + +func rewardValidators(g *Graph, snapshot *avl.Tree, root *Transaction, tx *Transaction, logging bool) error { + fee := sys.TransactionFeeAmount + + creatorBalance, _ := ReadAccountBalance(snapshot, tx.Creator) + + if creatorBalance < fee { + return errors.Errorf("stake: creator %x does not have enough PERLs to pay transaction fees (comprised of %d PERLs)", tx.Creator, fee) + } + + WriteAccountBalance(snapshot, tx.Creator, creatorBalance-fee) + + var candidates []*Transaction + var stakes []uint64 + var totalStake uint64 + + visited := make(map[TransactionID]struct{}) + + queue := AcquireQueue() + defer ReleaseQueue(queue) + + for _, parentID := range tx.ParentIDs { + if parent := g.FindTransaction(parentID); parent != nil { + queue.PushBack(parent) + } + + visited[parentID] = struct{}{} + } + + hasher, _ := blake2b.New256(nil) + + var depthCounter uint64 + var lastDepth = tx.Depth + + for queue.Len() > 0 { + popped := queue.PopFront().(*Transaction) + + if popped.Depth != lastDepth { + lastDepth = popped.Depth + depthCounter++ + } + + // If we exceed the max eligible depth we search for candidate + // validators to reward from, stop traversing. + + if depthCounter >= sys.MaxDepthDiff { + break + } + + // Filter for all ancestral transactions not from the same sender, + // and within the desired graph depth. + + if popped.Sender != tx.Sender { + stake, _ := ReadAccountStake(snapshot, popped.Sender) + + if stake > sys.MinimumStake { + candidates = append(candidates, popped) + stakes = append(stakes, stake) + + totalStake += stake + + // Record entropy source. + if _, err := hasher.Write(popped.ID[:]); err != nil { + return errors.Wrap(err, "stake: failed to hash transaction ID for entropy source") + } + } + } + + for _, parentID := range popped.ParentIDs { + if _, seen := visited[parentID]; !seen { + if parent := g.FindTransaction(parentID); parent != nil { + queue.PushBack(parent) + } + + visited[parentID] = struct{}{} + } + } + } + + // If there are no eligible rewardee candidates, do not reward anyone. + + if len(candidates) == 0 || len(stakes) == 0 || totalStake == 0 { + return nil + } + + entropy := hasher.Sum(nil) + acc, threshold := float64(0), float64(binary.LittleEndian.Uint64(entropy)%uint64(0xffff))/float64(0xffff) + + var rewardee *Transaction + + // Model a weighted uniform distribution by a random variable X, and select + // whichever validator has a weight X ≥ X' as a reward recipient. + + for i, tx := range candidates { + acc += float64(stakes[i]) / float64(totalStake) + + if acc >= threshold { + rewardee = tx + break + } + } + + // If there is no selected transaction that deserves a reward, give the + // reward to the last reward candidate. + + if rewardee == nil { + rewardee = candidates[len(candidates)-1] + } + + rewardeeBalance, _ := ReadAccountReward(snapshot, rewardee.Sender) + WriteAccountReward(snapshot, rewardee.Sender, rewardeeBalance+fee) + + if logging { + logger := log.Stake("reward_validator") + logger.Info(). + Hex("creator", tx.Creator[:]). + Hex("recipient", rewardee.Sender[:]). + Hex("creator_tx_id", tx.ID[:]). + Hex("rewardee_tx_id", rewardee.ID[:]). + Hex("entropy", entropy). + Float64("acc", acc). + Float64("threshold", threshold).Msg("Rewarded validator.") + } + + return nil +} + +func collapseTransactions(g *Graph, accounts *Accounts, round uint64, latestRound *Round, root *Transaction, end *Transaction, logging bool) (*collapseResults, error) { + res := &collapseResults{snapshot: accounts.Snapshot()} + res.snapshot.SetViewID(round) + + visited := map[TransactionID]struct{}{root.ID: {}} + + queue := queue2.New() + queue.PushBack(end) + + order := queue2.New() + + for queue.Len() > 0 { + popped := queue.PopFront().(*Transaction) + + if popped.Depth <= root.Depth { + continue + } + + order.PushBack(popped) + + for _, parentID := range popped.ParentIDs { + if _, seen := visited[parentID]; seen { + continue + } + + visited[parentID] = struct{}{} + + parent := g.FindTransaction(parentID) + + if parent == nil { + g.MarkTransactionAsMissing(parentID, popped.Depth) + return nil, errors.Errorf("missing ancestor %x to correctly collapse down ledger state from critical transaction %x", parentID, end.ID) + } + + queue.PushBack(parent) + } + } + + res.applied = make([]*Transaction, 0, order.Len()) + res.rejected = make([]*Transaction, 0, order.Len()) + res.rejectedErrors = make([]error, 0, order.Len()) + + // Apply transactions in reverse order from the end of the round + // all the way down to the beginning of the round. + + for order.Len() > 0 { + popped := order.PopBack().(*Transaction) + + // Update nonce. + + nonce, exists := ReadAccountNonce(res.snapshot, popped.Creator) + if !exists { + WriteAccountsLen(res.snapshot, ReadAccountsLen(res.snapshot)+1) + } + WriteAccountNonce(res.snapshot, popped.Creator, nonce+1) + + // FIXME(kenta): FOR TESTNET ONLY. FAUCET DOES NOT GET ANY PERLs DEDUCTED. + if hex.EncodeToString(popped.Creator[:]) != sys.FaucetAddress { + if err := rewardValidators(g, res.snapshot, root, popped, logging); err != nil { + res.rejected = append(res.rejected, popped) + res.rejectedErrors = append(res.rejectedErrors, err) + res.rejectedCount += popped.LogicalUnits() + + continue + } + } + + if err := ApplyTransaction(latestRound, res.snapshot, popped); err != nil { + res.rejected = append(res.rejected, popped) + res.rejectedErrors = append(res.rejectedErrors, err) + res.rejectedCount += popped.LogicalUnits() + + fmt.Println(err) + + continue + } + + // Update statistics. + + res.applied = append(res.applied, popped) + res.appliedCount += popped.LogicalUnits() + } + + startDepth, endDepth := root.Depth+1, end.Depth + + for _, tx := range g.GetTransactionsByDepth(&startDepth, &endDepth) { + res.ignoredCount += tx.LogicalUnits() + } + + res.ignoredCount -= res.appliedCount + res.rejectedCount + + if round >= uint64(sys.RewardWithdrawalsRoundLimit) { + processRewardWithdrawals(round, res.snapshot) + } + + return res, nil +} diff --git a/common.go b/common.go index 1a863769..dc11976b 100644 --- a/common.go +++ b/common.go @@ -27,14 +27,16 @@ import ( import _ "github.com/perlin-network/wavelet/internal/snappy" const ( - SizeTransactionID = blake2b.Size256 - SizeRoundID = blake2b.Size256 - SizeMerkleNodeID = md5.Size - SizeAccountID = 32 - SizeSignature = 64 + SizeTransactionID = blake2b.Size256 + SizeTransactionSeed = blake2b.Size256 + SizeRoundID = blake2b.Size256 + SizeMerkleNodeID = md5.Size + SizeAccountID = 32 + SizeSignature = 64 ) type TransactionID = [SizeTransactionID]byte +type TransactionSeed = [SizeTransactionSeed]byte type RoundID = [SizeRoundID]byte type MerkleNodeID = [SizeMerkleNodeID]byte type AccountID = [SizeAccountID]byte diff --git a/genesis.go b/genesis.go index 8405054c..40a7bf02 100644 --- a/genesis.go +++ b/genesis.go @@ -22,6 +22,7 @@ package wavelet import ( "encoding/hex" "github.com/perlin-network/wavelet/avl" + "github.com/perlin-network/wavelet/log" "github.com/pkg/errors" "github.com/valyala/fastjson" ) @@ -44,6 +45,8 @@ const defaultGenesis = ` // performInception loads data expected to exist at the birth of any node in this ledgers network. // The data is fed in as .json. func performInception(tree *avl.Tree, genesis *string) Round { + logger := log.Node() + var buf []byte if genesis != nil { @@ -57,13 +60,13 @@ func performInception(tree *avl.Tree, genesis *string) Round { parsed, err := p.ParseBytes(buf) if err != nil { - panic(err) + logger.Fatal().Err(err).Msg("ParseBytes()") } accounts, err := parsed.Object() if err != nil { - panic(err) + logger.Fatal().Err(err).Msg("parsed.Object()") } var balance, stake, reward uint64 @@ -146,11 +149,11 @@ func performInception(tree *avl.Tree, genesis *string) Round { }) if err != nil { - panic(err) + logger.Fatal().Err(err).Msg("accounts.Visit") } - tx := Transaction{} + tx := &Transaction{} tx.rehash() - return NewRound(0, tree.Checksum(), 0, Transaction{}, tx) + return NewRound(0, tree.Checksum(), 0, &Transaction{}, tx) } diff --git a/gossip.go b/gossip.go index 9d42c1ca..557bff0b 100644 --- a/gossip.go +++ b/gossip.go @@ -56,7 +56,7 @@ func NewGossiper(ctx context.Context, client *skademlia.Client, metrics *Metrics return g } -func (g *Gossiper) Push(tx Transaction) { +func (g *Gossiper) Push(tx *Transaction) { g.debouncer.Add(debounce.Bytes(tx.Marshal())) if g.metrics != nil { diff --git a/graph.go b/graph.go index c6c8bce7..1a90fd30 100644 --- a/graph.go +++ b/graph.go @@ -32,7 +32,7 @@ import ( type GraphOption func(*Graph) -func WithRoot(root Transaction) GraphOption { +func WithRoot(root *Transaction) GraphOption { return func(graph *Graph) { if graph.indexer != nil { graph.indexer.Index(hex.EncodeToString(root.ID[:])) @@ -127,7 +127,7 @@ func NewGraph(opts ...GraphOption) *Graph { // AddTransaction adds sufficiently valid transactions with a strongly connected ancestry // to the graph, and otherwise buffers incomplete transactions, or otherwise rejects // invalid transactions. -func (g *Graph) AddTransaction(tx Transaction) error { +func (g *Graph) AddTransaction(tx *Transaction) error { g.Lock() defer g.Unlock() @@ -143,9 +143,7 @@ func (g *Graph) AddTransaction(tx Transaction) error { return errors.Wrap(err, "failed to validate transaction") } - ptr := &tx - - g.transactions[tx.ID] = ptr + g.transactions[tx.ID] = tx delete(g.missing, tx.ID) parentsMissing := false @@ -177,7 +175,7 @@ func (g *Graph) AddTransaction(tx Transaction) error { return ErrMissingParents } - return g.updateGraph(ptr) + return g.updateGraph(tx) } // MarkTransactionAsMissing marks a transaction at some given depth to be @@ -192,15 +190,13 @@ func (g *Graph) MarkTransactionAsMissing(id TransactionID, depth uint64) { // UpdateRoot forcefully adds a root transaction to the graph, and updates all // relevant graph indices as a result of setting a new root with its new depth. -func (g *Graph) UpdateRoot(root Transaction) { - ptr := &root - +func (g *Graph) UpdateRoot(root *Transaction) { g.Lock() - g.depthIndex[root.Depth] = append(g.depthIndex[root.Depth], ptr) - g.eligibleIndex.ReplaceOrInsert((*sortByDepthTX)(ptr)) + g.depthIndex[root.Depth] = append(g.depthIndex[root.Depth], root) + g.eligibleIndex.ReplaceOrInsert((*sortByDepthTX)(root)) - g.transactions[root.ID] = ptr + g.transactions[root.ID] = root g.height = root.Depth + 1 @@ -612,7 +608,7 @@ func (g *Graph) deleteProgeny(id TransactionID) { } } -func (g *Graph) validateTransaction(tx Transaction) error { +func (g *Graph) validateTransaction(tx *Transaction) error { if tx.ID == ZeroTransactionID { return errors.New("tx must have an ID") } @@ -694,7 +690,7 @@ func (g *Graph) validateTransactionParents(tx *Transaction) error { var maxDepth uint64 - for _, parentID := range tx.ParentIDs { + for i, parentID := range tx.ParentIDs { parent, exists := g.transactions[parentID] if !exists { @@ -708,6 +704,10 @@ func (g *Graph) validateTransactionParents(tx *Transaction) error { if maxDepth < parent.Depth { // Update max depth witnessed from parents. maxDepth = parent.Depth } + + if parent.Seed != tx.ParentSeeds[i] { + return errors.New("parent seed mismatch") + } } maxDepth++ diff --git a/graph_test.go b/graph_test.go index 725a62e7..af4bce58 100644 --- a/graph_test.go +++ b/graph_test.go @@ -40,7 +40,7 @@ func TestNewGraph(t *testing.T) { eligible := graph.FindEligibleParents() assert.Len(t, eligible, 1) - assert.Equal(t, tx, *eligible[0]) + assert.Equal(t, tx, eligible[0]) tx2 := AttachSenderToTransaction(keys, NewTransaction(keys, sys.TagNop, nil), eligible...) @@ -69,7 +69,7 @@ func TestGraphFuzz(t *testing.T) { count := 1 for i := 0; i < 500; i++ { - var depth []Transaction + var depth []*Transaction for i := 0; i < rand.Intn(sys.MaxParentsPerTransaction)+1; i++ { var payload [50]byte @@ -95,14 +95,14 @@ func TestGraphFuzz(t *testing.T) { assert.Len(t, graph.incomplete, 0) assert.Len(t, graph.Missing(), 0) - var transactions []Transaction + var transactions []*Transaction for _, tx := range graph.transactions { if tx.ID == root.ID { continue } - transactions = append(transactions, *tx) + transactions = append(transactions, tx) } assert.Len(t, transactions, count-1) @@ -149,7 +149,7 @@ func TestGraphPruneBelowDepth(t *testing.T) { pruneDepth = graph.height } - var depth []Transaction + var depth []*Transaction for i := 0; i < rand.Intn(sys.MaxParentsPerTransaction)+1; i++ { var payload [50]byte @@ -205,7 +205,7 @@ func TestGraphUpdateRoot(t *testing.T) { graph := NewGraph(WithRoot(root)) for i := 0; i < 50; i++ { - var depth []Transaction + var depth []*Transaction for i := 0; i < rand.Intn(sys.MaxParentsPerTransaction)+1; i++ { var payload [50]byte @@ -266,7 +266,7 @@ func TestGraphValidateTransactionParents(t *testing.T) { graph := NewGraph(WithRoot(root)) for i := 0; i < 50; i++ { - var depth []Transaction + var depth []*Transaction for i := 0; i < rand.Intn(sys.MaxParentsPerTransaction)+1; i++ { var payload [50]byte @@ -283,12 +283,20 @@ func TestGraphValidateTransactionParents(t *testing.T) { } tx := AttachSenderToTransaction(keys, NewTransaction(keys, sys.TagNop, nil), graph.depthIndex[(graph.height-1)-(sys.MaxDepthDiff+2)][0]) + assert.NoError(t, graph.validateTransactionParents(tx)) + + assert.Equal(t, len(tx.ParentIDs), len(tx.ParentSeeds)) + + parentSeed := tx.ParentSeeds[0] + tx.ParentSeeds[0] = [32]byte{} + assert.Error(t, graph.validateTransactionParents(tx)) + tx.ParentSeeds[0] = parentSeed tx.Depth += sys.MaxDepthDiff - assert.True(t, errors.Cause(graph.validateTransactionParents(&tx)) == ErrDepthLimitExceeded) + assert.True(t, errors.Cause(graph.validateTransactionParents(tx)) == ErrDepthLimitExceeded) tx.Depth-- - assert.True(t, errors.Cause(graph.validateTransactionParents(&tx)) != ErrDepthLimitExceeded) + assert.True(t, errors.Cause(graph.validateTransactionParents(tx)) != ErrDepthLimitExceeded) } func TestGraphFindEligibleCritical(t *testing.T) { @@ -318,7 +326,7 @@ func TestGraphFindEligibleCritical(t *testing.T) { } assert.NoError(t, graph.AddTransaction(eligible)) - assert.Equal(t, *graph.FindEligibleCritical(difficulty), eligible) + assert.Equal(t, graph.FindEligibleCritical(difficulty), eligible) root = AttachSenderToTransaction(keys, NewTransaction(keys, sys.TagNop, nil)) graph = NewGraph(WithRoot(root)) @@ -336,7 +344,7 @@ func TestGraphFindEligibleCriticalInBigGraph(t *testing.T) { difficulty := byte(8) - var eligible Transaction + var eligible *Transaction for i := 0; i < 500; i++ { if i == 500/3 { // Prune away any eligible critical transactions a third through the graph. @@ -360,7 +368,7 @@ func TestGraphFindEligibleCriticalInBigGraph(t *testing.T) { assert.NoError(t, graph.AddTransaction(eligible)) } - var depth []Transaction + var depth []*Transaction for i := 0; i < rand.Intn(sys.MaxParentsPerTransaction)+1; i++ { var payload [50]byte @@ -389,5 +397,5 @@ func TestGraphFindEligibleCriticalInBigGraph(t *testing.T) { } } - assert.Equal(t, *graph.FindEligibleCritical(difficulty), eligible) + assert.Equal(t, graph.FindEligibleCritical(difficulty), eligible) } diff --git a/ledger.go b/ledger.go index 8335ae8e..6b285cf6 100644 --- a/ledger.go +++ b/ledger.go @@ -31,7 +31,6 @@ import ( "github.com/perlin-network/wavelet/log" "github.com/perlin-network/wavelet/store" "github.com/perlin-network/wavelet/sys" - queue2 "github.com/phf/go-queue/queue" "github.com/pkg/errors" "golang.org/x/crypto/blake2b" "google.golang.org/grpc" @@ -72,6 +71,8 @@ type Ledger struct { } func NewLedger(kv store.KV, client *skademlia.Client, genesis *string) *Ledger { + logger := log.Node() + metrics := NewMetrics(context.TODO()) indexer := NewIndexer() @@ -85,13 +86,13 @@ func NewLedger(kv store.KV, client *skademlia.Client, genesis *string) *Ledger { if rounds != nil && err != nil { genesis := performInception(accounts.tree, genesis) if err := accounts.Commit(nil); err != nil { - panic(err) + logger.Fatal().Err(err).Msg("BUG: accounts.Commit") } ptr := &genesis if _, err := rounds.Save(ptr); err != nil { - panic(err) + logger.Fatal().Err(err).Msg("BUG: rounds.Save") } round = ptr @@ -100,7 +101,7 @@ func NewLedger(kv store.KV, client *skademlia.Client, genesis *string) *Ledger { } if round == nil { - panic("???: COULD NOT FIND GENESIS, OR STORAGE IS CORRUPTED.") + logger.Fatal().Err(err).Msg("BUG: COULD NOT FIND GENESIS, OR STORAGE IS CORRUPTED.") } graph := NewGraph(WithMetrics(metrics), WithIndexer(indexer), WithRoot(round.End), VerifySignatures()) @@ -145,7 +146,7 @@ func NewLedger(kv store.KV, client *skademlia.Client, genesis *string) *Ledger { // invalid or fails any validation checks, an error is returned. No error // is returned if the transaction has already existed int he ledgers graph // beforehand. -func (l *Ledger) AddTransaction(tx Transaction) error { +func (l *Ledger) AddTransaction(tx *Transaction) error { err := l.graph.AddTransaction(tx) if err != nil && errors.Cause(err) != ErrAlreadyExists { @@ -446,13 +447,13 @@ FINALIZE_ROUNDS: continue FINALIZE_ROUNDS } - results, err := l.CollapseTransactions(current.Index+1, current.End, *eligible, false) + results, err := l.collapseTransactions(current.Index+1, current.End, eligible, false) if err != nil { fmt.Println(err) continue } - candidate := NewRound(current.Index+1, results.snapshot.Checksum(), uint64(results.appliedCount), current.End, *eligible) + candidate := NewRound(current.Index+1, results.snapshot.Checksum(), uint64(results.appliedCount), current.End, eligible) l.finalizer.Prefer(&candidate) continue FINALIZE_ROUNDS @@ -544,7 +545,7 @@ FINALIZE_ROUNDS: return } - results, err := l.CollapseTransactions(round.Index, round.Start, round.End, false) + results, err := l.collapseTransactions(round.Index, round.Start, round.End, false) if err != nil { if !strings.Contains(err.Error(), "missing ancestor") { fmt.Println(err) @@ -612,7 +613,7 @@ FINALIZE_ROUNDS: finalized := l.finalizer.Preferred() l.finalizer.Reset() - results, err := l.CollapseTransactions(finalized.Index, finalized.Start, finalized.End, true) + results, err := l.collapseTransactions(finalized.Index, finalized.Start, finalized.End, true) if err != nil { if !strings.Contains(err.Error(), "missing ancestor") { fmt.Println(err) @@ -1118,7 +1119,8 @@ func (l *Ledger) SyncToLatestRound() { l.graph.UpdateRoot(latest.End) if err := l.accounts.Commit(snapshot); err != nil { - panic(errors.Wrap(err, "failed to commit collapsed state to our database")) + logger := log.Node() + logger.Fatal().Err(err).Msg("failed to commit collapsed state to our database") } logger = log.Sync("apply") @@ -1138,45 +1140,10 @@ func (l *Ledger) SyncToLatestRound() { } } -// ApplyTransactionToSnapshot applies a transactions intended changes to a snapshot -// of the ledgers current state. -func (l *Ledger) ApplyTransactionToSnapshot(snapshot *avl.Tree, tx *Transaction) error { - round := l.Rounds().Latest() - original := snapshot.Snapshot() - - switch tx.Tag { - case sys.TagNop: - case sys.TagTransfer: - if _, err := ApplyTransferTransaction(snapshot, round, tx, nil); err != nil { - snapshot.Revert(original) - - fmt.Println(err) - return errors.Wrap(err, "could not apply transfer transaction") - } - case sys.TagStake: - if _, err := ApplyStakeTransaction(snapshot, round, tx); err != nil { - snapshot.Revert(original) - return errors.Wrap(err, "could not apply stake transaction") - } - case sys.TagContract: - if _, err := ApplyContractTransaction(snapshot, round, tx, nil); err != nil { - snapshot.Revert(original) - return errors.Wrap(err, "could not apply contract transaction") - } - case sys.TagBatch: - if _, err := ApplyBatchTransaction(snapshot, round, tx); err != nil { - snapshot.Revert(original) - return errors.Wrap(err, "could not apply batch transaction") - } - } - - return nil -} - -// CollapseResults is what is returned by calling CollapseTransactions. Refer to CollapseTransactions +// collapseResults is what returned by calling collapseTransactions. Refer to collapseTransactions // to understand what counts of accepted, rejected, or otherwise ignored transactions truly represent -// after calling CollapseTransactions. -type CollapseResults struct { +// after calling collapseTransactions. +type collapseResults struct { applied []*Transaction rejected []*Transaction rejectedErrors []error @@ -1188,7 +1155,7 @@ type CollapseResults struct { snapshot *avl.Tree } -// CollapseTransactions takes all transactions recorded within a graph depth interval, and applies +// collapseTransactions takes all transactions recorded within a graph depth interval, and applies // all valid and available ones to a snapshot of all accounts stored in the ledger. It returns // an updated snapshot with all finalized transactions applied, alongside count summaries of the // number of applied, rejected, or otherwise ignored transactions. @@ -1202,8 +1169,8 @@ type CollapseResults struct { // It is important to note that transactions that are inspected over are specifically transactions // that are within the depth interval (start, end] where start is the interval starting point depth, // and end is the interval ending point depth. -func (l *Ledger) CollapseTransactions(round uint64, root Transaction, end Transaction, logging bool) (*CollapseResults, error) { - var res *CollapseResults +func (l *Ledger) collapseTransactions(round uint64, root *Transaction, end *Transaction, logging bool) (*collapseResults, error) { + var res *collapseResults defer func() { if res != nil && logging { @@ -1218,102 +1185,14 @@ func (l *Ledger) CollapseTransactions(round uint64, root Transaction, end Transa }() if results, exists := l.cacheCollapse.load(end.ID); exists { - res = results.(*CollapseResults) + res = results.(*collapseResults) return res, nil } - res = &CollapseResults{snapshot: l.accounts.Snapshot()} - res.snapshot.SetViewID(round) - - visited := map[TransactionID]struct{}{root.ID: {}} - - queue := queue2.New() - queue.PushBack(&end) - - order := queue2.New() - - for queue.Len() > 0 { - popped := queue.PopFront().(*Transaction) - - if popped.Depth <= root.Depth { - continue - } - - order.PushBack(popped) - - for _, parentID := range popped.ParentIDs { - if _, seen := visited[parentID]; seen { - continue - } - - visited[parentID] = struct{}{} - - parent := l.graph.FindTransaction(parentID) - - if parent == nil { - l.graph.MarkTransactionAsMissing(parentID, popped.Depth) - return nil, errors.Errorf("missing ancestor %x to correctly collapse down ledger state from critical transaction %x", parentID, end.ID) - } - - queue.PushBack(parent) - } - } - - res.applied = make([]*Transaction, 0, order.Len()) - res.rejected = make([]*Transaction, 0, order.Len()) - res.rejectedErrors = make([]error, 0, order.Len()) - - // Apply transactions in reverse order from the end of the round - // all the way down to the beginning of the round. - - for order.Len() > 0 { - popped := order.PopBack().(*Transaction) - - // Update nonce. - - nonce, exists := ReadAccountNonce(res.snapshot, popped.Creator) - if !exists { - WriteAccountsLen(res.snapshot, ReadAccountsLen(res.snapshot)+1) - } - WriteAccountNonce(res.snapshot, popped.Creator, nonce+1) - - // FIXME(kenta): FOR TESTNET ONLY. FAUCET DOES NOT GET ANY PERLs DEDUCTED. - if hex.EncodeToString(popped.Creator[:]) != sys.FaucetAddress { - if err := l.RewardValidators(res.snapshot, root, popped, logging); err != nil { - res.rejected = append(res.rejected, popped) - res.rejectedErrors = append(res.rejectedErrors, err) - res.rejectedCount += popped.LogicalUnits() - - continue - } - } - - if err := l.ApplyTransactionToSnapshot(res.snapshot, popped); err != nil { - res.rejected = append(res.rejected, popped) - res.rejectedErrors = append(res.rejectedErrors, err) - res.rejectedCount += popped.LogicalUnits() - - fmt.Println(err) - - continue - } - - // Update statistics. - - res.applied = append(res.applied, popped) - res.appliedCount += popped.LogicalUnits() - } - - startDepth, endDepth := root.Depth+1, end.Depth - - for _, tx := range l.graph.GetTransactionsByDepth(&startDepth, &endDepth) { - res.ignoredCount += tx.LogicalUnits() - } - - res.ignoredCount -= res.appliedCount + res.rejectedCount - - if round >= uint64(sys.RewardWithdrawalsRoundLimit) { - l.processRewardWithdrawals(round, res.snapshot) + var err error + res, err = collapseTransactions(l.graph, l.accounts, round, l.Rounds().Latest(), root, end, logging) + if err != nil { + return nil, err } l.cacheCollapse.put(end.ID, res) @@ -1371,140 +1250,3 @@ func (l *Ledger) LogChanges(snapshot *avl.Tree, lastRound uint64) { return true }) } - -func (l *Ledger) processRewardWithdrawals(round uint64, snapshot *avl.Tree) { - rws := GetRewardWithdrawalRequests(snapshot, round-uint64(sys.RewardWithdrawalsRoundLimit)) - - for _, rw := range rws { - balance, _ := ReadAccountBalance(snapshot, rw.account) - WriteAccountBalance(snapshot, rw.account, balance+rw.amount) - - snapshot.Delete(rw.Key()) - } -} - -func (l *Ledger) RewardValidators(snapshot *avl.Tree, root Transaction, tx *Transaction, logging bool) error { - fee := sys.TransactionFeeAmount - - creatorBalance, _ := ReadAccountBalance(snapshot, tx.Creator) - - if creatorBalance < fee { - return errors.Errorf("stake: creator %x does not have enough PERLs to pay transaction fees (comprised of %d PERLs)", tx.Creator, fee) - } - - WriteAccountBalance(snapshot, tx.Creator, creatorBalance-fee) - - var candidates []*Transaction - var stakes []uint64 - var totalStake uint64 - - visited := make(map[TransactionID]struct{}) - - queue := AcquireQueue() - defer ReleaseQueue(queue) - - for _, parentID := range tx.ParentIDs { - if parent := l.graph.FindTransaction(parentID); parent != nil { - queue.PushBack(parent) - } - - visited[parentID] = struct{}{} - } - - hasher, _ := blake2b.New256(nil) - - var depthCounter uint64 - var lastDepth = tx.Depth - - for queue.Len() > 0 { - popped := queue.PopFront().(*Transaction) - - if popped.Depth != lastDepth { - lastDepth = popped.Depth - depthCounter++ - } - - // If we exceed the max eligible depth we search for candidate - // validators to reward from, stop traversing. - - if depthCounter >= sys.MaxDepthDiff { - break - } - - // Filter for all ancestral transactions not from the same sender, - // and within the desired graph depth. - - if popped.Sender != tx.Sender { - stake, _ := ReadAccountStake(snapshot, popped.Sender) - - if stake > sys.MinimumStake { - candidates = append(candidates, popped) - stakes = append(stakes, stake) - - totalStake += stake - - // Record entropy source. - if _, err := hasher.Write(popped.ID[:]); err != nil { - return errors.Wrap(err, "stake: failed to hash transaction ID for entropy source") - } - } - } - - for _, parentID := range popped.ParentIDs { - if _, seen := visited[parentID]; !seen { - if parent := l.graph.FindTransaction(parentID); parent != nil { - queue.PushBack(parent) - } - - visited[parentID] = struct{}{} - } - } - } - - // If there are no eligible rewardee candidates, do not reward anyone. - - if len(candidates) == 0 || len(stakes) == 0 || totalStake == 0 { - return nil - } - - entropy := hasher.Sum(nil) - acc, threshold := float64(0), float64(binary.LittleEndian.Uint64(entropy)%uint64(0xffff))/float64(0xffff) - - var rewardee *Transaction - - // Model a weighted uniform distribution by a random variable X, and select - // whichever validator has a weight X ≥ X' as a reward recipient. - - for i, tx := range candidates { - acc += float64(stakes[i]) / float64(totalStake) - - if acc >= threshold { - rewardee = tx - break - } - } - - // If there is no selected transaction that deserves a reward, give the - // reward to the last reward candidate. - - if rewardee == nil { - rewardee = candidates[len(candidates)-1] - } - - rewardeeBalance, _ := ReadAccountReward(snapshot, rewardee.Sender) - WriteAccountReward(snapshot, rewardee.Sender, rewardeeBalance+fee) - - if logging { - logger := log.Stake("reward_validator") - logger.Info(). - Hex("creator", tx.Creator[:]). - Hex("recipient", rewardee.Sender[:]). - Hex("creator_tx_id", tx.ID[:]). - Hex("rewardee_tx_id", rewardee.ID[:]). - Hex("entropy", entropy). - Float64("acc", acc). - Float64("threshold", threshold).Msg("Rewarded validator.") - } - - return nil -} diff --git a/round.go b/round.go index 8eda2a53..2e4eddba 100644 --- a/round.go +++ b/round.go @@ -40,11 +40,11 @@ type Round struct { Applied uint64 - Start Transaction - End Transaction + Start *Transaction + End *Transaction } -func NewRound(index uint64, merkle MerkleNodeID, applied uint64, start, end Transaction) Round { +func NewRound(index uint64, merkle MerkleNodeID, applied uint64, start, end *Transaction) Round { r := Round{ Index: index, Merkle: merkle, diff --git a/rounds_test.go b/rounds_test.go index 55f1cb7d..2a91bbaa 100644 --- a/rounds_test.go +++ b/rounds_test.go @@ -41,6 +41,8 @@ func TestNewRounds(t *testing.T) { for i := 0; i < 5; i++ { r := &Round{ Index: uint64(i + 1), + Start: &Transaction{}, + End: &Transaction{}, } _, err := rm.Save(r) assert.NoError(t, err) @@ -78,6 +80,8 @@ func TestRoundsCircular(t *testing.T) { for i := 0; i < 15; i++ { r := &Round{ Index: uint64(i + 1), + Start: &Transaction{}, + End: &Transaction{}, } _, err := rm.Save(r) assert.NoError(t, err) diff --git a/site/docs/transactions.md b/site/docs/transactions.md index d0410fe5..108c3924 100644 --- a/site/docs/transactions.md +++ b/site/docs/transactions.md @@ -73,6 +73,7 @@ The current binary format of a Wavelet transaction is denoted as follows: | Creator Account ID | 256-bit wallet address/public key. | | Nonce | Latest nonce value of the creators account, denoted as an unsigned 64-bit little-endian integer. | | Parent IDs | Length-prefixed array of 256-bit transaction IDs; assigned by the transactions sender. | +| Parent Seeds | Array of 256-bit transaction seeds, with the same length as the Parent IDs field and therefore not length-prefixed; must correspond to the transactions specified by Parent IDs. | | Depth | Unsigned 64-bit little-endian integer; assigned by the transactions sender. | | Tag | 8-bit integer (byte) identifying the transactions operation. | | Payload | Length-prefixed array of bytes providing further details of the operation invoked under the transactions designated tag. | diff --git a/testdata/recursive_invocation.wasm b/testdata/recursive_invocation.wasm new file mode 100644 index 00000000..304b7d47 Binary files /dev/null and b/testdata/recursive_invocation.wasm differ diff --git a/testdata/transfer_back.wasm b/testdata/transfer_back.wasm new file mode 100644 index 00000000..cd0e3002 Binary files /dev/null and b/testdata/transfer_back.wasm differ diff --git a/tx.go b/tx.go index 714062fa..7d11f04a 100644 --- a/tx.go +++ b/tx.go @@ -23,16 +23,16 @@ import ( "bytes" "encoding/binary" "fmt" - "io" - "math" - "math/bits" - "sort" - "github.com/perlin-network/noise/edwards25519" "github.com/perlin-network/noise/skademlia" + "github.com/perlin-network/wavelet/log" "github.com/perlin-network/wavelet/sys" "github.com/pkg/errors" "golang.org/x/crypto/blake2b" + "io" + "math" + "math/bits" + "sort" ) type Transaction struct { @@ -41,7 +41,8 @@ type Transaction struct { Nonce uint64 - ParentIDs []TransactionID // Transactions parents. + ParentIDs []TransactionID // Transactions parents. + ParentSeeds []TransactionSeed Depth uint64 // Graph depth. @@ -53,12 +54,12 @@ type Transaction struct { ID TransactionID // BLAKE2b(*). - Seed [blake2b.Size256]byte // BLAKE2b(Sender || ParentIDs) - SeedLen byte // Number of prefixed zeroes of BLAKE2b(Sender || ParentIDs). + Seed TransactionSeed // BLAKE2b(Sender || ParentIDs) + SeedLen byte // Number of prefixed zeroes of BLAKE2b(Sender || ParentIDs). } -func NewTransaction(creator *skademlia.Keypair, tag sys.Tag, payload []byte) Transaction { - tx := Transaction{Tag: tag, Payload: payload} +func NewTransaction(creator *skademlia.Keypair, tag sys.Tag, payload []byte) *Transaction { + tx := &Transaction{Tag: tag, Payload: payload} var nonce [8]byte // TODO(kenta): nonce @@ -68,13 +69,15 @@ func NewTransaction(creator *skademlia.Keypair, tag sys.Tag, payload []byte) Tra return tx } -func NewBatchTransaction(creator *skademlia.Keypair, tags []byte, payloads [][]byte) Transaction { +func NewBatchTransaction(creator *skademlia.Keypair, tags []byte, payloads [][]byte) *Transaction { + logger := log.TX("new_batch") + if len(tags) != len(payloads) { - panic("UNEXPECTED: Number of tags must be equivalent to number of payloads.") + logger.Fatal().Msg("UNEXPECTED: Number of tags must be equivalent to number of payloads.") } if len(tags) == math.MaxUint8 { - panic("UNEXPECTED: Total number of tags/payloads in a single batch transaction is 255.") + logger.Fatal().Msg("UNEXPECTED: Total number of tags/payloads in a single batch transaction is 255.") } var size [4]byte @@ -91,12 +94,22 @@ func NewBatchTransaction(creator *skademlia.Keypair, tags []byte, payloads [][]b return NewTransaction(creator, sys.TagBatch, append([]byte{byte(len(tags))}, buf...)) } -func AttachSenderToTransaction(sender *skademlia.Keypair, tx Transaction, parents ...*Transaction) Transaction { +// Attaches sender to a transaction without modifying it in-place. +func AttachSenderToTransaction(sender *skademlia.Keypair, _tx *Transaction, parents ...*Transaction) *Transaction { + tx := *_tx + AppendSenderToTransaction(sender, &tx, parents...) + return &tx +} + +// Appends sender to a transaction in-place. +func AppendSenderToTransaction(sender *skademlia.Keypair, tx *Transaction, parents ...*Transaction) { if len(parents) > 0 { tx.ParentIDs = make([]TransactionID, 0, len(parents)) + tx.ParentSeeds = make([]TransactionSeed, 0, len(parents)) for _, parent := range parents { tx.ParentIDs = append(tx.ParentIDs, parent.ID) + tx.ParentSeeds = append(tx.ParentSeeds, parent.Seed) if tx.Depth < parent.Depth { tx.Depth = parent.Depth @@ -114,26 +127,46 @@ func AttachSenderToTransaction(sender *skademlia.Keypair, tx Transaction, parent tx.SenderSignature = edwards25519.Sign(sender.PrivateKey(), tx.Marshal()) tx.rehash() - - return tx } -func (t *Transaction) rehash() *Transaction { +func (t *Transaction) rehash() { + logger := log.Node() + t.ID = blake2b.Sum256(t.Marshal()) - buf := make([]byte, 0, SizeAccountID+len(t.ParentIDs)*SizeTransactionID) - buf = append(buf, t.Sender[:]...) - for _, parentID := range t.ParentIDs { - buf = append(buf, parentID[:]...) - } + // Calculate the new seed. + { + hasher, err := blake2b.New(32, nil) + if err != nil { + logger.Fatal().Err(err).Msg("BUG: blake2b.New") // should never happen + } - t.Seed = blake2b.Sum256(buf) - t.SeedLen = byte(prefixLen(t.Seed[:])) + _, err = hasher.Write(t.Sender[:]) + if err != nil { + logger.Fatal().Err(err).Msg("BUG: hasher.Write (1)") // should never happen + } - return t + for _, parentSeed := range t.ParentSeeds { + _, err = hasher.Write(parentSeed[:]) + if err != nil { + logger.Fatal().Err(err).Msg("BUG: hasher.Write (2)") // should never happen + } + } + + // Write 8-bit hash of transaction content to reduce conflicts. + _, err = hasher.Write(t.ID[:1]) + if err != nil { + logger.Fatal().Err(err).Msg("BUG: hasher.Write (3)") // should never happen + } + + seed := hasher.Sum(nil) + copy(t.Seed[:], seed) + + t.SeedLen = byte(prefixLen(seed)) + } } -func (t Transaction) Marshal() []byte { +func (t *Transaction) Marshal() []byte { w := bytes.NewBuffer(make([]byte, 0, 222+(32*len(t.ParentIDs))+len(t.Payload))) w.Write(t.Sender[:]) @@ -154,6 +187,9 @@ func (t Transaction) Marshal() []byte { for _, parentID := range t.ParentIDs { w.Write(parentID[:]) } + for _, parentSeed := range t.ParentSeeds { + w.Write(parentSeed[:]) + } binary.BigEndian.PutUint64(buf[:8], t.Depth) w.Write(buf[:8]) @@ -173,7 +209,9 @@ func (t Transaction) Marshal() []byte { return w.Bytes() } -func UnmarshalTransaction(r io.Reader) (t Transaction, err error) { +func UnmarshalTransaction(r io.Reader) (t *Transaction, err error) { + t = &Transaction{} + if _, err = io.ReadFull(r, t.Sender[:]); err != nil { err = errors.Wrap(err, "failed to decode transaction sender") return @@ -228,6 +266,15 @@ func UnmarshalTransaction(r io.Reader) (t Transaction, err error) { } } + t.ParentSeeds = make([]TransactionSeed, len(t.ParentIDs)) + + for i := range t.ParentSeeds { + if _, err = io.ReadFull(r, t.ParentSeeds[i][:]); err != nil { + err = errors.Wrapf(err, "failed to decode parent seed %d", i) + return + } + } + if _, err = io.ReadFull(r, buf[:8]); err != nil { err = errors.Wrap(err, "could not read transaction depth") return @@ -273,23 +320,13 @@ func UnmarshalTransaction(r io.Reader) (t Transaction, err error) { return t, nil } -func prefixLen(buf []byte) int { - for i, b := range buf { - if b != 0 { - return i*8 + bits.LeadingZeros8(uint8(b)) - } - } - - return len(buf)*8 - 1 -} - -func (tx Transaction) IsCritical(difficulty byte) bool { +func (tx *Transaction) IsCritical(difficulty byte) bool { return tx.SeedLen >= difficulty } // LogicalUnits counts the total number of atomic logical units of changes // the specified tx comprises of. -func (tx Transaction) LogicalUnits() int { +func (tx *Transaction) LogicalUnits() int { if tx.Tag != sys.TagBatch { return 1 } @@ -303,6 +340,16 @@ func (tx Transaction) LogicalUnits() int { return int(buf[0]) } -func (tx Transaction) String() string { +func (tx *Transaction) String() string { return fmt.Sprintf("Transaction{ID: %x}", tx.ID) } + +func prefixLen(buf []byte) int { + for i, b := range buf { + if b != 0 { + return i*8 + bits.LeadingZeros8(uint8(b)) + } + } + + return len(buf)*8 - 1 +} diff --git a/tx_applier.go b/tx_applier.go index b102b891..ca869346 100644 --- a/tx_applier.go +++ b/tx_applier.go @@ -21,145 +21,103 @@ package wavelet import ( "encoding/hex" - "fmt" - "github.com/perlin-network/wavelet/avl" "github.com/perlin-network/wavelet/log" "github.com/perlin-network/wavelet/sys" "github.com/pkg/errors" ) -type ContractExecutorState struct { - Sender AccountID - GasLimit uint64 +type contractExecutorState struct { + GasPayer AccountID + GasLimit uint64 + GasLimitIsSet bool } -func ApplyTransferTransaction(snapshot *avl.Tree, round *Round, tx *Transaction, state *ContractExecutorState) (*avl.Tree, error) { - params, err := ParseTransferTransaction(tx.Payload) - if err != nil { - return nil, err - } - - code, codeAvailable := ReadAccountContractCode(snapshot, params.Recipient) - - if !codeAvailable && (params.GasLimit > 0 || len(params.FuncName) > 0 || len(params.FuncParams) > 0) { - return nil, errors.New("transfer: transactions to non-contract accounts should not specify gas limit or function names or params") - } - - senderBalance, _ := ReadAccountBalance(snapshot, tx.Creator) - - // FIXME(kenta): FOR TESTNET ONLY. FAUCET DOES NOT GET ANY PERLs DEDUCTED. - if hex.EncodeToString(tx.Creator[:]) == sys.FaucetAddress { - recipientBalance, _ := ReadAccountBalance(snapshot, params.Recipient) - WriteAccountBalance(snapshot, params.Recipient, recipientBalance+params.Amount) - - return snapshot, nil - } - - if senderBalance < params.Amount { - return nil, errors.Errorf("transfer: %x tried send %d PERLs to %x, but only has %d PERLs", - tx.Creator, params.Amount, params.Recipient, senderBalance) - } - - if !codeAvailable { - WriteAccountBalance(snapshot, tx.Creator, senderBalance-params.Amount) +func ApplyTransaction(round *Round, state *avl.Tree, tx *Transaction) error { + return applyTransaction(round, state, tx, &contractExecutorState{ + GasPayer: tx.Creator, + }) +} - recipientBalance, _ := ReadAccountBalance(snapshot, params.Recipient) - WriteAccountBalance(snapshot, params.Recipient, recipientBalance+params.Amount) +func applyTransaction(round *Round, state *avl.Tree, tx *Transaction, execState *contractExecutorState) error { + original := state.Snapshot() - return snapshot, nil + switch tx.Tag { + case sys.TagNop: + case sys.TagTransfer: + if err := applyTransferTransaction(state, round, tx, execState); err != nil { + state.Revert(original) + return errors.Wrap(err, "could not apply transfer transaction") + } + case sys.TagStake: + if err := applyStakeTransaction(state, round, tx); err != nil { + state.Revert(original) + return errors.Wrap(err, "could not apply stake transaction") + } + case sys.TagContract: + if err := applyContractTransaction(state, round, tx, execState); err != nil { + state.Revert(original) + return errors.Wrap(err, "could not apply contract transaction") + } + case sys.TagBatch: + if err := applyBatchTransaction(state, round, tx, execState); err != nil { + state.Revert(original) + return errors.Wrap(err, "could not apply batch transaction") + } } - sender := tx.Creator - if state != nil { - sender = state.Sender - params.GasLimit = state.GasLimit - } + return nil +} - if params.GasLimit == 0 { - return nil, errors.New("transfer: gas limit for invoking smart contract must be greater than zero") +func applyTransferTransaction(snapshot *avl.Tree, round *Round, tx *Transaction, state *contractExecutorState) error { + params, err := ParseTransferTransaction(tx.Payload) + if err != nil { + return err } - senderBalance, _ = ReadAccountBalance(snapshot, sender) + code, codeAvailable := ReadAccountContractCode(snapshot, params.Recipient) - if senderBalance < params.GasLimit { - return nil, errors.Errorf("transfer: %x attempted to claim a gas limit of %d PERLs, but only has %d PERLs", - sender, params.GasLimit, senderBalance) + if !codeAvailable && (params.GasLimit > 0 || len(params.FuncName) > 0 || len(params.FuncParams) > 0) { + return errors.New("transfer: transactions to non-contract accounts should not specify gas limit or function names or params") } - WriteAccountBalance(snapshot, tx.Creator, senderBalance-params.Amount) + // senderBalance/recipientBalance should be hidden from other code. + { + senderBalance, _ := ReadAccountBalance(snapshot, tx.Creator) - recipientBalance, _ := ReadAccountBalance(snapshot, params.Recipient) - WriteAccountBalance(snapshot, params.Recipient, recipientBalance+params.Amount) + // FIXME(kenta): FOR TESTNET ONLY. FAUCET DOES NOT GET ANY PERLs DEDUCTED. + if hex.EncodeToString(tx.Creator[:]) == sys.FaucetAddress { + recipientBalance, _ := ReadAccountBalance(snapshot, params.Recipient) + WriteAccountBalance(snapshot, params.Recipient, recipientBalance+params.Amount) - executor := &ContractExecutor{} + return nil + } - if err := executor.Execute(snapshot, params.Recipient, round, tx, params.Amount, params.GasLimit, string(params.FuncName), params.FuncParams, code); err != nil { - return nil, errors.Wrap(err, "transfer: failed to invoke smart contract") - } + if senderBalance < params.Amount { + return errors.Errorf("transfer: %x tried send %d PERLs to %x, but only has %d PERLs", + tx.Creator, params.Amount, params.Recipient, senderBalance) + } - if executor.GasLimitExceeded { // Revert changes and have the sender pay gas fees. - WriteAccountBalance(snapshot, tx.Creator, senderBalance-executor.Gas) + senderBalance -= params.Amount + WriteAccountBalance(snapshot, tx.Creator, senderBalance) recipientBalance, _ := ReadAccountBalance(snapshot, params.Recipient) + recipientBalance += params.Amount WriteAccountBalance(snapshot, params.Recipient, recipientBalance) + } - logger := log.Contracts("gas") - logger.Info(). - Hex("sender_id", tx.Creator[:]). - Hex("contract_id", params.Recipient[:]). - Uint64("gas", executor.Gas). - Uint64("gas_limit", params.GasLimit). - Msg("Exceeded gas limit while invoking smart contract function.") - } else { - WriteAccountBalance(snapshot, tx.Creator, senderBalance-params.Amount-executor.Gas) - - logger := log.Contracts("gas") - logger.Info(). - Hex("sender_id", tx.Creator[:]). - Hex("contract_id", params.Recipient[:]). - Uint64("gas", executor.Gas). - Uint64("gas_limit", params.GasLimit). - Msg("Deducted PERLs for invoking smart contract function.") - - if state == nil { - state = &ContractExecutorState{Sender: tx.Sender} - } - - if params.GasLimit > executor.Gas { - state.GasLimit = params.GasLimit - executor.Gas - } - - for _, entry := range executor.Queue { - switch entry.Tag { - case sys.TagNop: - case sys.TagTransfer: - if _, err := ApplyTransferTransaction(snapshot, round, entry, state); err != nil { - return nil, err - } - case sys.TagStake: - if _, err := ApplyStakeTransaction(snapshot, round, entry); err != nil { - return nil, err - } - case sys.TagContract: - if _, err := ApplyContractTransaction(snapshot, round, entry, state); err != nil { - return nil, err - } - case sys.TagBatch: - if _, err := ApplyBatchTransaction(snapshot, round, entry); err != nil { - return nil, err - } - } - } + if !codeAvailable { + return nil } - return snapshot, nil + err = executeContractInTransactionContext(tx, params.Recipient, code, snapshot, round, params.Amount, params.GasLimit, params.FuncName, params.FuncParams, state) + return err } -func ApplyStakeTransaction(snapshot *avl.Tree, round *Round, tx *Transaction) (*avl.Tree, error) { +func applyStakeTransaction(snapshot *avl.Tree, round *Round, tx *Transaction) error { params, err := ParseStakeTransaction(tx.Payload) if err != nil { - return nil, err + return err } balance, _ := ReadAccountBalance(snapshot, tx.Creator) @@ -169,25 +127,25 @@ func ApplyStakeTransaction(snapshot *avl.Tree, round *Round, tx *Transaction) (* switch params.Opcode { case sys.PlaceStake: if balance < params.Amount { - return nil, errors.Errorf("stake: %x attempt to place a stake of %d PERLs, but only has %d PERLs", tx.Creator, params.Amount, balance) + return errors.Errorf("stake: %x attempt to place a stake of %d PERLs, but only has %d PERLs", tx.Creator, params.Amount, balance) } WriteAccountBalance(snapshot, tx.Creator, balance-params.Amount) WriteAccountStake(snapshot, tx.Creator, stake+params.Amount) case sys.WithdrawStake: if stake < params.Amount { - return nil, errors.Errorf("stake: %x attempt to withdraw a stake of %d PERLs, but only has staked %d PERLs", tx.Creator, params.Amount, stake) + return errors.Errorf("stake: %x attempt to withdraw a stake of %d PERLs, but only has staked %d PERLs", tx.Creator, params.Amount, stake) } WriteAccountBalance(snapshot, tx.Creator, balance+params.Amount) WriteAccountStake(snapshot, tx.Creator, stake-params.Amount) case sys.WithdrawReward: if params.Amount < sys.MinimumRewardWithdraw { - return nil, errors.Errorf("stake: %x attempt to withdraw rewards amounting to %d PERLs, but system requires the minimum amount to withdraw to be %d PERLs", tx.Creator, params.Amount, sys.MinimumRewardWithdraw) + return errors.Errorf("stake: %x attempt to withdraw rewards amounting to %d PERLs, but system requires the minimum amount to withdraw to be %d PERLs", tx.Creator, params.Amount, sys.MinimumRewardWithdraw) } if reward < params.Amount { - return nil, errors.Errorf("stake: %x attempt to withdraw rewards amounting to %d PERLs, but only has rewards amounting to %d PERLs", tx.Creator, params.Amount, reward) + return errors.Errorf("stake: %x attempt to withdraw rewards amounting to %d PERLs, but only has rewards amounting to %d PERLs", tx.Creator, params.Amount, reward) } WriteAccountReward(snapshot, tx.Creator, reward-params.Amount) @@ -198,121 +156,130 @@ func ApplyStakeTransaction(snapshot *avl.Tree, round *Round, tx *Transaction) (* }) } - return snapshot, nil + return nil } -func ApplyContractTransaction(snapshot *avl.Tree, round *Round, tx *Transaction, state *ContractExecutorState) (*avl.Tree, error) { +func applyContractTransaction(snapshot *avl.Tree, round *Round, tx *Transaction, state *contractExecutorState) error { params, err := ParseContractTransaction(tx.Payload) if err != nil { - return nil, err + return err } if _, exists := ReadAccountContractNumPages(snapshot, tx.ID); exists { - return nil, errors.New("contract: already exists") + return errors.New("contract: already exists") } - sender := tx.Creator - if state != nil { - sender = state.Sender - params.GasLimit = state.GasLimit - } + WriteAccountContractCode(snapshot, tx.ID, params.Code) + err = executeContractInTransactionContext(tx, AccountID(tx.ID), params.Code, snapshot, round, 0, params.GasLimit, []byte("init"), params.Params, state) + return err +} - if params.GasLimit == 0 { - return nil, errors.New("contract: gas limit for invoking smart contract must be greater than zero") +func applyBatchTransaction(snapshot *avl.Tree, round *Round, tx *Transaction, state *contractExecutorState) error { + params, err := ParseBatchTransaction(tx.Payload) + if err != nil { + return err } - balance, _ := ReadAccountBalance(snapshot, sender) - - if balance < params.GasLimit { - return nil, errors.Errorf("contract: %x tried to spawn a contract using a gas limit of %d PERLs but only has %d PERLs", sender, params.GasLimit, balance) + for i := uint8(0); i < params.Size; i++ { + entry := &Transaction{ + ID: tx.ID, + Sender: tx.Sender, + Creator: tx.Creator, + Nonce: tx.Nonce, + Tag: sys.Tag(params.Tags[i]), + Payload: params.Payloads[i], + } + if err := applyTransaction(round, snapshot, entry, state); err != nil { + return errors.Wrapf(err, "Error while processing %d/%d transaction in a batch.", i+1, params.Size) + } } - executor := &ContractExecutor{} + return nil +} - if err := executor.Execute(snapshot, tx.ID, round, tx, 0, params.GasLimit, `init`, params.Params, params.Code); err != nil { - return nil, errors.Wrap(err, "contract: failed to init smart contract") +func executeContractInTransactionContext( + tx *Transaction, + contractID AccountID, + code []byte, + snapshot *avl.Tree, + round *Round, + amount uint64, + requestedGasLimit uint64, + funcName []byte, + funcParams []byte, + state *contractExecutorState, +) error { + logger := log.Contracts("execute") + + gasPayer := state.GasPayer + gasPayerBalance, _ := ReadAccountBalance(snapshot, gasPayer) + + if !state.GasLimitIsSet { + state.GasLimit = requestedGasLimit + state.GasLimitIsSet = true } - WriteAccountBalance(snapshot, tx.Creator, balance-executor.Gas) - - if !executor.GasLimitExceeded { - if state == nil { - state = &ContractExecutorState{Sender: tx.Sender} - } + realGasLimit := state.GasLimit - if params.GasLimit > executor.Gas { - state.GasLimit = params.GasLimit - executor.Gas - } + if requestedGasLimit < realGasLimit { + realGasLimit = requestedGasLimit + } - for _, entry := range executor.Queue { - switch entry.Tag { - case sys.TagNop: - case sys.TagTransfer: - if _, err := ApplyTransferTransaction(snapshot, round, entry, state); err != nil { - return nil, err - } - case sys.TagStake: - if _, err := ApplyStakeTransaction(snapshot, round, entry); err != nil { - return nil, err - } - case sys.TagContract: - if _, err := ApplyContractTransaction(snapshot, round, entry, state); err != nil { - return nil, err - } - case sys.TagBatch: - if _, err := ApplyBatchTransaction(snapshot, round, entry); err != nil { - return nil, err - } - } - } + if realGasLimit == 0 { + return errors.New("execute_contract: gas limit for invoking smart contract must be greater than zero") + } - WriteAccountContractCode(snapshot, tx.ID, params.Code) + if gasPayerBalance < realGasLimit { + return errors.Errorf("execute_contract: attempted to deduct gas fee from %x of %d PERLs, but only has %d PERLs", + gasPayer, realGasLimit, gasPayerBalance) } - logger := log.Contracts("gas") - logger.Info(). - Hex("creator_id", tx.Creator[:]). - Hex("contract_id", tx.ID[:]). - Uint64("gas", executor.Gas). - Uint64("gas_limit", params.GasLimit). - Msg("Deducted PERLs for spawning a smart contract.") + executor := &ContractExecutor{} + snapshotBeforeExec := snapshot.Snapshot() - return snapshot, nil -} + invocationErr := executor.Execute(snapshot, contractID, round, tx, amount, realGasLimit, string(funcName), funcParams, code) -func ApplyBatchTransaction(snapshot *avl.Tree, round *Round, tx *Transaction) (*avl.Tree, error) { - params, err := ParseBatchTransaction(tx.Payload) - if err != nil { - return nil, err + // gasPayerBalance >= realGasLimit >= executor.Gas && state.GasLimit >= realGasLimit must always hold. + if realGasLimit < executor.Gas { + logger.Fatal().Msg("BUG: realGasLimit < executor.Gas") + } + if state.GasLimit < realGasLimit { + logger.Fatal().Msg("BUG: state.GasLimit < realGasLimit") } - for i := uint8(0); i < params.Size; i++ { - entry := &Transaction{ - ID: tx.ID, - Sender: tx.Sender, - Creator: tx.Creator, - Nonce: tx.Nonce, - Tag: sys.Tag(params.Tags[i]), - Payload: params.Payloads[i], + if executor.GasLimitExceeded || invocationErr != nil { // Revert changes and have the gas payer pay gas fees. + snapshot.Revert(snapshotBeforeExec) + WriteAccountBalance(snapshot, gasPayer, gasPayerBalance-executor.Gas) + state.GasLimit -= executor.Gas + + if invocationErr != nil { + logger.Info().Err(invocationErr).Msg("failed to invoke smart contract") + } else { + logger.Info(). + Hex("sender_id", tx.Creator[:]). + Hex("contract_id", contractID[:]). + Uint64("gas", executor.Gas). + Uint64("gas_limit", realGasLimit). + Msg("Exceeded gas limit while invoking smart contract function.") } + } else { + WriteAccountBalance(snapshot, gasPayer, gasPayerBalance-executor.Gas) + state.GasLimit -= executor.Gas - switch entry.Tag { - case sys.TagNop: - case sys.TagTransfer: - if _, err := ApplyTransferTransaction(snapshot, round, entry, nil); err != nil { - return nil, err - } - case sys.TagStake: - if _, err := ApplyStakeTransaction(snapshot, round, entry); err != nil { - fmt.Println(err) - return nil, err - } - case sys.TagContract: - if _, err := ApplyContractTransaction(snapshot, round, entry, nil); err != nil { - return nil, err + logger.Info(). + Hex("sender_id", tx.Creator[:]). + Hex("contract_id", contractID[:]). + Uint64("gas", executor.Gas). + Uint64("gas_limit", realGasLimit). + Msg("Deducted PERLs for invoking smart contract function.") + + for _, entry := range executor.Queue { + err := applyTransaction(round, snapshot, entry, state) + if err != nil { + logger.Info().Err(err).Msg("failed to process sub-transaction") } } } - return snapshot, nil + return nil } diff --git a/tx_applier_test.go b/tx_applier_test.go new file mode 100644 index 00000000..fd0eb49f --- /dev/null +++ b/tx_applier_test.go @@ -0,0 +1,393 @@ +// Copyright (c) 2019 Perlin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package wavelet + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/perlin-network/noise/skademlia" + "github.com/perlin-network/wavelet/avl" + "github.com/perlin-network/wavelet/store" + "github.com/perlin-network/wavelet/sys" + "github.com/stretchr/testify/assert" + "io/ioutil" + "math/rand" + "testing" +) + +type TxApplierTestAccount struct { + keys *skademlia.Keypair + effect struct { + Balance uint64 + Stake uint64 + } +} + +func TestApplyTransaction_Single(t *testing.T) { + const InitialBalance = 100000000 + + state := avl.New(store.NewInmem()) + var initialRoot *Transaction + + viewID := uint64(0) + state.SetViewID(viewID) + + accounts := make(map[AccountID]*TxApplierTestAccount) + accountIDs := make([]AccountID, 0) + for i := 0; i < 60; i++ { + keys, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + account := &TxApplierTestAccount{ + keys: keys, + } + if i == 0 { + initialRoot = AttachSenderToTransaction(keys, NewTransaction(keys, sys.TagNop, nil)) + } + WriteAccountBalance(state, keys.PublicKey(), InitialBalance) + account.effect.Balance = InitialBalance + + accounts[keys.PublicKey()] = account + accountIDs = append(accountIDs, keys.PublicKey()) + } + + rng := rand.New(rand.NewSource(42)) + + round := NewRound(viewID, state.Checksum(), 0, &Transaction{}, initialRoot) + + for i := 0; i < 10000; i++ { + switch rng.Intn(2) { + case 0: + amount := rng.Uint64()%100 + 1 + account := accounts[accountIDs[rng.Intn(len(accountIDs))]] + account.effect.Stake += amount + account.effect.Balance -= amount + + tx := AttachSenderToTransaction(account.keys, NewTransaction(account.keys, sys.TagStake, buildPlaceStakePayload(amount))) + err := ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + case 1: + amount := rng.Uint64()%100 + 1 + fromAccount := accounts[accountIDs[rng.Intn(len(accountIDs))]] + toAccount := accounts[accountIDs[rng.Intn(len(accountIDs))]] + if fromAccount.keys.PublicKey() == toAccount.keys.PublicKey() { + continue + } + + toAccountID := toAccount.keys.PublicKey() + + fromAccount.effect.Balance -= amount + toAccount.effect.Balance += amount + + tx := AttachSenderToTransaction(fromAccount.keys, NewTransaction(fromAccount.keys, sys.TagTransfer, buildTransferPayload(toAccountID, amount))) + err := ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + default: + panic("unreachable") + } + + } + + for id, account := range accounts { + stake, _ := ReadAccountStake(state, id) + assert.Equal(t, stake, account.effect.Stake) + + balance, _ := ReadAccountBalance(state, id) + assert.Equal(t, balance, account.effect.Balance) + } +} + +func TestApplyTransaction_Collapse(t *testing.T) { + const InitialBalance = 100000000 + + stateStore := store.NewInmem() + state := avl.New(stateStore) + var initialRoot *Transaction + + viewID := uint64(0) + state.SetViewID(viewID) + + var graph *Graph + + accounts := make(map[AccountID]*TxApplierTestAccount) + accountIDs := make([]AccountID, 0) + for i := 0; i < 60; i++ { + keys, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + account := &TxApplierTestAccount{ + keys: keys, + } + if i == 0 { + initialRoot = AttachSenderToTransaction(keys, NewTransaction(keys, sys.TagNop, nil)) + graph = NewGraph(WithRoot(initialRoot)) + } + WriteAccountBalance(state, keys.PublicKey(), InitialBalance) + account.effect.Balance = InitialBalance + + accounts[keys.PublicKey()] = account + accountIDs = append(accountIDs, keys.PublicKey()) + } + + rng := rand.New(rand.NewSource(42)) + round := NewRound(viewID, state.Checksum(), 0, &Transaction{}, initialRoot) + accountState := NewAccounts(stateStore) + accountState.Commit(state) + + var criticalCount int + + for criticalCount < 100 { + amount := rng.Uint64()%100 + 1 + account := accounts[accountIDs[rng.Intn(len(accountIDs))]] + account.effect.Stake += amount + + tx := AttachSenderToTransaction(account.keys, NewTransaction(account.keys, sys.TagStake, buildPlaceStakePayload(amount)), graph.FindEligibleParents()...) + err := graph.AddTransaction(tx) + assert.NoError(t, err) + if tx.IsCritical(4) { + results, err := collapseTransactions(graph, accountState, viewID+1, &round, round.End, tx, false) + assert.NoError(t, err) + err = accountState.Commit(results.snapshot) + assert.NoError(t, err) + state = results.snapshot + round = NewRound(viewID+1, state.Checksum(), uint64(results.appliedCount), round.End, tx) + viewID += 1 + + for id, account := range accounts { + stake, _ := ReadAccountStake(state, id) + assert.Equal(t, stake, account.effect.Stake) + } + criticalCount++ + } + } +} + +func TestApplyTransferTransaction(t *testing.T) { + state := avl.New(store.NewInmem()) + round := NewRound(0, state.Checksum(), 0, &Transaction{}, &Transaction{}) + alice, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + bob, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + + aliceID := alice.PublicKey() + bobID := bob.PublicKey() + + // Case 1 - Success + WriteAccountBalance(state, aliceID, 1) + + tx := AttachSenderToTransaction(alice, NewTransaction(alice, sys.TagTransfer, buildTransferPayload(bobID, 1))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + // Case 2 - Not enough balance + tx = AttachSenderToTransaction(alice, NewTransaction(alice, sys.TagTransfer, buildTransferPayload(bobID, 1))) + err = ApplyTransaction(&round, state, tx) + assert.Error(t, err) + + // Case 3 - Self-transfer without enough balance + tx = AttachSenderToTransaction(alice, NewTransaction(alice, sys.TagTransfer, buildTransferPayload(aliceID, 1))) + err = ApplyTransaction(&round, state, tx) + assert.Error(t, err) +} + +func TestApplyStakeTransaction(t *testing.T) { + state := avl.New(store.NewInmem()) + round := NewRound(0, state.Checksum(), 0, &Transaction{}, &Transaction{}) + account, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + + accountID := account.PublicKey() + + // Case 1 - Placement success + WriteAccountBalance(state, accountID, 100) + + tx := AttachSenderToTransaction(account, NewTransaction(account, sys.TagStake, buildPlaceStakePayload(100))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + // Case 2 - Not enough balance + tx = AttachSenderToTransaction(account, NewTransaction(account, sys.TagStake, buildPlaceStakePayload(100))) + err = ApplyTransaction(&round, state, tx) + assert.Error(t, err) + + // Case 3 - Withdrawal success + tx = AttachSenderToTransaction(account, NewTransaction(account, sys.TagStake, buildWithdrawStakePayload(100))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + finalBalance, _ := ReadAccountBalance(state, accountID) + assert.Equal(t, finalBalance, uint64(100)) +} + +func TestApplyBatchTransaction(t *testing.T) { + state := avl.New(store.NewInmem()) + round := NewRound(0, state.Checksum(), 0, &Transaction{}, &Transaction{}) + alice, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + bob, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + + aliceID := alice.PublicKey() + bobID := bob.PublicKey() + + WriteAccountBalance(state, aliceID, 100) + + // initial stake + tx := AttachSenderToTransaction(alice, NewTransaction(alice, sys.TagStake, buildPlaceStakePayload(100))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + // this implies order + tx = AttachSenderToTransaction(alice, NewBatchTransaction( + alice, + []byte{byte(sys.TagStake), byte(sys.TagTransfer)}, + [][]byte{buildWithdrawStakePayload(100), buildTransferPayload(bobID, 100)}, + )) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + finalBobBalance, _ := ReadAccountBalance(state, bobID) + assert.Equal(t, finalBobBalance, uint64(100)) +} + +func TestApplyContractTransaction(t *testing.T) { + state := avl.New(store.NewInmem()) + round := NewRound(0, state.Checksum(), 0, &Transaction{}, &Transaction{}) + account, err := skademlia.NewKeys(1, 1) + assert.NoError(t, err) + + accountID := account.PublicKey() + + code, err := ioutil.ReadFile("testdata/transfer_back.wasm") + assert.NoError(t, err) + + // Case 1 - balance < gas_fee + WriteAccountBalance(state, accountID, 99999) + tx := AttachSenderToTransaction(account, NewTransaction(account, sys.TagContract, buildContractSpawnPayload(100000, code))) + err = ApplyTransaction(&round, state, tx) + assert.Error(t, err) + + // Case 2 - Success + WriteAccountBalance(state, accountID, 100000) + tx = AttachSenderToTransaction(account, NewTransaction(account, sys.TagContract, buildContractSpawnPayload(100000, code))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + finalBalance, _ := ReadAccountBalance(state, accountID) + assert.Condition(t, func() bool { return finalBalance > 0 && finalBalance < 100000 }) + + // Try to transfer some money + WriteAccountBalance(state, accountID, 1000000000) + tx = AttachSenderToTransaction(account, NewTransaction(account, sys.TagTransfer, buildTransferWithInvocationPayload( + AccountID(tx.ID), + 200000000, + 500000, + []byte("on_money_received"), + nil, + ))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + finalBalance, _ = ReadAccountBalance(state, accountID) + assert.Condition(t, func() bool { return finalBalance > 1000000000-100000000-500000 && finalBalance < 1000000000-100000000 }) + + code, err = ioutil.ReadFile("testdata/recursive_invocation.wasm") + assert.NoError(t, err) + + WriteAccountBalance(state, accountID, 100000000) + tx = AttachSenderToTransaction(account, NewTransaction(account, sys.TagContract, buildContractSpawnPayload(100000, code))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + recursiveInvocationContractID := AccountID(tx.ID) + + WriteAccountBalance(state, accountID, 6000000) + tx = AttachSenderToTransaction(account, NewTransaction(account, sys.TagTransfer, buildTransferWithInvocationPayload( + recursiveInvocationContractID, + 0, + 5000000, + []byte("bomb"), + recursiveInvocationContractID[:], + ))) + err = ApplyTransaction(&round, state, tx) + assert.NoError(t, err) + + finalBalance, _ = ReadAccountBalance(state, accountID) + fmt.Println(finalBalance) + assert.Condition(t, func() bool { return finalBalance >= 1000000 && finalBalance < 1100000 }) // GasLimit specified in contract is 100000 +} + +func buildTransferWithInvocationPayload(dest AccountID, amount uint64, gasLimit uint64, funcName []byte, param []byte) []byte { + payload := bytes.NewBuffer(nil) + payload.Write(dest[:]) + var intBuf [8]byte + binary.LittleEndian.PutUint64(intBuf[:], amount) + payload.Write(intBuf[:]) + + binary.LittleEndian.PutUint64(intBuf[:], gasLimit) + payload.Write(intBuf[:]) + + binary.LittleEndian.PutUint32(intBuf[:4], uint32(len(funcName))) + payload.Write(intBuf[:4]) + payload.Write(funcName) + + binary.LittleEndian.PutUint32(intBuf[:4], uint32(len(param))) + payload.Write(intBuf[:4]) + payload.Write(param) + + return payload.Bytes() +} + +func buildContractSpawnPayload(gasLimit uint64, code []byte) []byte { + var buf [8]byte + w := bytes.NewBuffer(nil) + binary.LittleEndian.PutUint64(buf[:], gasLimit) // Gas fee. + w.Write(buf[:]) + binary.LittleEndian.PutUint32(buf[:4], 0) // Payload size. + w.Write(buf[:4]) + + w.Write(code) // Smart contract code. + return w.Bytes() +} + +func buildTransferPayload(dest AccountID, amount uint64) []byte { + payload := bytes.NewBuffer(nil) + payload.Write(dest[:]) + var intBuf [8]byte + binary.LittleEndian.PutUint64(intBuf[:], amount) + payload.Write(intBuf[:]) + return payload.Bytes() +} + +func buildPlaceStakePayload(amount uint64) []byte { + var intBuf [8]byte + payload := bytes.NewBuffer(nil) + payload.WriteByte(sys.PlaceStake) + binary.LittleEndian.PutUint64(intBuf[:8], uint64(amount)) + payload.Write(intBuf[:8]) + return payload.Bytes() +} + +func buildWithdrawStakePayload(amount uint64) []byte { + var intBuf [8]byte + payload := bytes.NewBuffer(nil) + payload.WriteByte(sys.WithdrawStake) + binary.LittleEndian.PutUint64(intBuf[:8], uint64(amount)) + payload.Write(intBuf[:8]) + return payload.Bytes() +}