Commit e9783459 authored by David Vorick's avatar David Vorick

add tests for multiple miner payouts

parent 2e5b3088
......@@ -14,11 +14,8 @@ principles.
TODO: Write the formal specification for encoding things.
TODO: Write the formal specification for deriving the block id, contract id,
siacoin output id, siafund output id, siafund claim output id.
TODO: block id + Merkle root, miner output ids, siacoin output ids, contract
ids, storage proof ids, siafund output ids, siafund claim output ids.
TODO: contract ids, storage proof ids, siafund output ids, siafund claim output
ids.
TODO: Document which crypto is used in consensus. (Hash algorithm, signature
algorithm)
......@@ -146,10 +143,10 @@ siacoin outputs, and contract payouts. There can be no leftovers.
The sum of all siafund inputs must equal the sum of all siafund outputs.
Inputs
------
Siacoin Inputs
--------------
Each input spends an output. The output being spent must already exist in the
Each input spends an output. The output being spent must already exist in the
state. An output has a value, and a spend hash (or address), which is the hash
of the 'spend conditions' object of the output. The spend conditions contain a
timelock, a number of required signatures, and a set of public keys that can be
......@@ -174,13 +171,16 @@ Miner Fees
A miner fee is an output that gets added directly to the block subsidy.
Outputs
-------
Siacoin Outputs
---------------
Outputs contain a value and a spend hash (also called a coin address). The
spend hash is a hash of the spend conditions that must be met to spend the
output.
The id of an output is obtained by marshalling and hashing all of the fields in
the transaction that contained the output, except for the signatures.
File Contracts
--------------
......@@ -198,7 +198,9 @@ when the storage proof is provided. If the storage proof is provides, the
payout goes to 'ValidProofAddress'. If no proof is submitted by block height
'End', then the payout goes to 'MissedProofAddress'.
All contracts must have a non-zero payout.
All contracts must have a non-zero payout, 'Start' must be before 'End', and
'Start' must be greater than the current height of the state. A storage proof
is acceptible if it is submitted in the block of height 'End'.
Storage Proofs
--------------
......@@ -206,8 +208,7 @@ Storage Proofs
A storage proof transaction is any transaction containing a storage proof.
Storage proof transactions are not allowed to have siacoin or siafund outputs,
and are not allowed to have file contracts. All outputs created by storage
proofs cannot be spent for 100 blocks.
and are not allowed to have file contracts.
When creating a storage proof, you only prove that you have a single 64 byte
segment of the file. The piece that you must prove you have is chosen
......@@ -303,6 +304,10 @@ malleable. This does however allow other parites to add additional inputs,
fees, etc. after you have signed the transaction without invalidating your
signature.
The 'Covered Fields' struct contains a slice of indexes for each element of the
transaction (siacoin inputs, miner fees, etc.). The slice must be sorted, and
there can be no repeated elements.
Entirely nonmalleable transactions can be achieved by setting the 'whole
transaction' flag and then providing the last signature, including every other
signature in your signature. Because no frivilous signatures are allowed, the
......
......@@ -31,6 +31,7 @@ var (
FutureBlockErr = errors.New("timestamp too far in future, will try again later.")
KnownOrphanErr = errors.New("block is a known orphan")
LargeBlockErr = errors.New("block is too large to be accepted")
MinerPayoutErr = errors.New("miner payout sum does not equal block subsidy")
MissedTargetErr = errors.New("block does not meet target")
UnknownOrphanErr = errors.New("block is an unknown orphan")
)
......@@ -111,7 +112,7 @@ func (s *State) checkMinerPayouts(b Block) (err error) {
// Return an error if the subsidy isn't equal to the payouts.
if subsidy != payoutSum {
err = errors.New("block miner payouts do not equal the block subsidy")
err = MinerPayoutErr
return
}
......
......@@ -153,6 +153,172 @@ func testLargeBlock(t *testing.T, s *State) {
}
}
// testMinerPayouts tries to submit miner payouts in various legal and illegal
// forms and verifies that the state handles the payouts correctly each time.
//
// CONTRIBUTE: Increased testing would be nice. We need to test across multiple
// payouts, multiple fees, payouts that are too high, payouts that are too low,
// and several other potential ways that someone might slip illegal payouts
// through.
func testMinerPayouts(t *testing.T, s *State) {
// Create a block with a single legal payout, no miner fees. The payout
// goes to the hash of the empty spend conditions.
var sc SpendConditions
payout := []Output{Output{Value: CalculateCoinbase(s.Height() + 1), SpendHash: sc.CoinAddress()}}
b, err := mineTestingBlock(s.CurrentBlock().ID(), Timestamp(time.Now().Unix()), payout, nil, s.CurrentTarget())
if err != nil {
t.Fatal(err)
}
err = s.AcceptBlock(b)
if err != nil {
t.Error(err)
}
// Check that the payout made it into the output list.
_, exists := s.unspentOutputs[b.MinerPayoutID(0)]
if !exists {
t.Error("miner payout not found in the list of unspent outputs")
}
// Create a block with multiple miner payouts.
payout = []Output{
Output{Value: CalculateCoinbase(s.Height()+1) - 750, SpendHash: sc.CoinAddress()},
Output{Value: 250, SpendHash: sc.CoinAddress()},
Output{Value: 500, SpendHash: sc.CoinAddress()},
}
b, err = mineTestingBlock(s.CurrentBlock().ID(), Timestamp(time.Now().Unix()), payout, nil, s.CurrentTarget())
if err != nil {
t.Fatal(err)
}
err = s.AcceptBlock(b)
if err != nil {
t.Error(err)
}
// Check that all three payouts made it into the output list.
_, exists = s.unspentOutputs[b.MinerPayoutID(0)]
if !exists {
t.Error("miner payout not found in the list of unspent outputs")
}
_, exists = s.unspentOutputs[b.MinerPayoutID(1)]
output250 := b.MinerPayoutID(1)
if !exists {
t.Error("miner payout not found in the list of unspent outputs")
}
_, exists = s.unspentOutputs[b.MinerPayoutID(2)]
output500 := b.MinerPayoutID(2)
if !exists {
t.Error("miner payout not found in the list of unspent outputs")
}
// Create a block with a too large payout.
payout = []Output{Output{Value: CalculateCoinbase(s.Height()), SpendHash: sc.CoinAddress()}}
b, err = mineTestingBlock(s.CurrentBlock().ID(), Timestamp(time.Now().Unix()), payout, nil, s.CurrentTarget())
if err != nil {
t.Fatal(err)
}
err = s.AcceptBlock(b)
if err != MinerPayoutErr {
t.Error("Unexpected Error:", err)
}
// Check that the payout did not make it into the output list.
_, exists = s.unspentOutputs[b.MinerPayoutID(0)]
if exists {
t.Error("miner payout made it into state despite being invalid.")
}
// Create a block with a too small payout.
payout = []Output{Output{Value: CalculateCoinbase(s.Height() + 2), SpendHash: sc.CoinAddress()}}
b, err = mineTestingBlock(s.CurrentBlock().ID(), Timestamp(time.Now().Unix()), payout, nil, s.CurrentTarget())
if err != nil {
t.Fatal(err)
}
err = s.AcceptBlock(b)
if err != MinerPayoutErr {
t.Error("Unexpected Error:", err)
}
// Check that the payout did not make it into the output list.
_, exists = s.unspentOutputs[b.MinerPayoutID(0)]
if exists {
t.Error("miner payout made it into state despite being invalid.")
}
// Test legal multiple payouts when there are multiple miner fees.
txn1 := Transaction{
Inputs: []Input{
Input{OutputID: output250},
},
MinerFees: []Currency{
Currency(50),
Currency(75),
Currency(125),
},
}
txn2 := Transaction{
Inputs: []Input{
Input{OutputID: output500},
},
MinerFees: []Currency{
Currency(100),
Currency(150),
Currency(250),
},
}
payout = []Output{Output{Value: CalculateCoinbase(s.Height()+1) + 25}, Output{Value: 650, SpendHash: sc.CoinAddress()}, Output{Value: 75, SpendHash: sc.CoinAddress()}}
b, err = mineTestingBlock(s.CurrentBlock().ID(), Timestamp(time.Now().Unix()), payout, []Transaction{txn1, txn2}, s.CurrentTarget())
if err != nil {
t.Fatal(err)
}
err = s.AcceptBlock(b)
if err != nil {
t.Error(err)
}
// Check that the payout outputs made it into the state.
_, exists = s.unspentOutputs[b.MinerPayoutID(0)]
if !exists {
t.Error("miner payout did not make it into the state")
}
_, exists = s.unspentOutputs[b.MinerPayoutID(1)]
output650 := b.MinerPayoutID(1)
if !exists {
t.Error("miner payout did not make it into the state")
}
_, exists = s.unspentOutputs[b.MinerPayoutID(2)]
output75 := b.MinerPayoutID(2)
if !exists {
t.Error("miner payout did not make it into the state")
}
// Test too large multiple payouts when there are multiple miner fees.
txn1 = Transaction{
Inputs: []Input{
Input{OutputID: output650},
},
MinerFees: []Currency{
Currency(100),
Currency(50),
Currency(500),
},
}
txn2 = Transaction{
Inputs: []Input{
Input{OutputID: output75},
},
MinerFees: []Currency{
Currency(10),
Currency(15),
Currency(50),
},
}
payout = []Output{Output{Value: CalculateCoinbase(s.Height()+1) + 25}, Output{Value: 650, SpendHash: sc.CoinAddress()}, Output{Value: 75, SpendHash: sc.CoinAddress()}}
b, err = mineTestingBlock(s.CurrentBlock().ID(), Timestamp(time.Now().Unix()), payout, []Transaction{txn1, txn2}, s.CurrentTarget())
if err != nil {
t.Fatal(err)
}
err = s.AcceptBlock(b)
if err != MinerPayoutErr {
t.Error("Expecting different error:", err)
}
}
// testMissedTarget tries to submit a block that does not meet the target for the next block.
func testMissedTarget(t *testing.T, s *State) {
// Mine a block that doesn't meet the target.
......@@ -407,6 +573,12 @@ func TestLargeBlock(t *testing.T) {
testLargeBlock(t, s)
}
// TestMinerPayouts creates a new state and uses it to call testMinerPayouts.
func TestMinerPayouts(t *testing.T) {
s := CreateGenesisState()
testMinerPayouts(t, s)
}
// TestMissedTarget creates a new state and uses it to call testMissedTarget.
func TestMissedTarget(t *testing.T) {
s := CreateGenesisState()
......@@ -436,8 +608,8 @@ func TestRepeatBlock(t *testing.T) {
// transactions, and invalid forms of each. Bad outputs, many outputs, many
// inputs, many fees, bad fees, overflows, bad proofs, early proofs, arbitrary
// datas, bad signatures, too many signatures, repeat signatures.
// TODO: Build those transaction building functions as separate things, because
//
// Build those transaction building functions as separate things, because
// you want to be able to probe complex transactions that have lots of juicy
// stuff.
......@@ -445,12 +617,17 @@ func TestRepeatBlock(t *testing.T) {
// state hashes always end up at the same point, make sure long forking works
// well and that reversing when a transaction fails also works well.
// TODO: Test the actually method which is used to calculated the earliest
// legal timestamp for the next block. Like have some examples that should work
// out algebraically and make sure that earliest timestamp follows the rules
// layed out by the protocol. This should be done after we decide that the
// algorithm for calculating the earliest allowed timestamp is sufficient.
// TODO: Test the actual method which is used to calculated the earliest legal
// timestamp for the next block. Like have some examples that should work out
// algebraically and make sure that earliest timestamp follows the rules layed
// out by the protocol. This should be done after we decide that the algorithm
// for calculating the earliest allowed timestamp is sufficient.
// TODO: Probe the difficulty adjustments, make sure that they are happening
// TODO: Probe the target adjustments, make sure that they are happening
// according to specification, moving as much as they should and that the
// clamps are being effective.
// TODO: Write tests for CalculateCoinbase to make sure that it's behaving
// correctly. I guess this doesn't help us if the state stops using
// CalculateCoinbase, but I'm not sure what other good alternative ways there
// are for testing this.
......@@ -41,7 +41,10 @@ func (t Transaction) OutputSum() (sum Currency) {
func (s *State) ValidSignatures(t Transaction) error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.validSignatures(t)
}
func (s *State) validSignatures(t Transaction) error {
// Create the InputSignatures object for each input.
sigMap := make(map[OutputID]*InputSignatures)
for i, input := range t.Inputs {
......@@ -156,7 +159,7 @@ func (s *State) validTransaction(t Transaction) (err error) {
}
// Check all of the signatures for validity.
err = s.ValidSignatures(t)
err = s.validSignatures(t)
if err != nil {
return
}
......
......@@ -2,6 +2,18 @@ package consensus
// TODO: Convert all string literals to byte arrays, or get rid of them entirely.
// TODO: Swtich to 128 bit Currency, which is overflow-safe. Then update
// CalculateCoinbase.
// TODO: Enable siafund stuff
// TODO: Enforce the 100 block spending hold on certain types of outputs: Miner
// payouts, storage proof outputs, siafund claims.
// TODO: Enforce restrictions on CoveredFields
// TODO: Enforce restrictions on which storage proof transactions are legal
import (
"github.com/NebulousLabs/Sia/crypto"
"github.com/NebulousLabs/Sia/encoding"
......@@ -207,13 +219,19 @@ func (t Transaction) FileContractID(i int) ContractID {
)))
}
// OuptutID takes the index of the output and returns the output's ID.
//
// TODO: ID should not include the signatures.
// OutputID gets the id of an output in the transaction, which is derived from
// marshalling all of the fields in the transaction except for the signatures
// and then appending the index of the output.
func (t Transaction) OutputID(i int) OutputID {
return OutputID(hash.HashBytes(encoding.MarshalAll(
t,
"coinsend",
t.Inputs,
t.MinerFees,
t.Outputs,
t.FileContracts,
t.StorageProofs,
// t.SiafundInputs,
// t.SiafundOutputs,
t.ArbitraryData,
i,
)))
}
......
......@@ -54,7 +54,6 @@ func New(state *consensus.State, tpool modules.TransactionPool, wallet modules.W
iterationsPerAttempt: 256 * 1024,
}
// Subscribe to the state and tpool.
addr, _, err := m.wallet.CoinAddress()
if err != nil {
return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment