From c0242f3883ddf6f33a43cfb61001a4fde4b51e42 Mon Sep 17 00:00:00 2001 From: Eric Rykwalder Date: Wed, 1 Mar 2017 16:04:36 -0800 Subject: [PATCH 1/4] protocol/bc: add SpendCommitment type Spend TxInputs need to contain all of the data necessary to reconstruct output ids in order to spend them. Performing the hashing during validation gives us cryptographic integrity that the spent output data is correct. In order to preserve this data, the mapping process was updated to return additional information which is stored in the account_utxos table. This data is then populated in a SpendCommitment type on the TxInput structs. --- core/account/builder.go | 2 +- core/account/builder_test.go | 12 +-- core/account/indexer.go | 25 ++++-- core/account/indexer_test.go | 2 +- core/account/reserve.go | 25 +++++- core/account/reserve_test.go | 6 +- core/coretest/fixtures.go | 4 +- core/migrate/data.go | 6 ++ core/query/annotated.go | 10 +-- core/query/index.go | 5 +- core/schema.sql | 6 +- core/txbuilder/txbuilder_test.go | 33 ++++---- core/txbuilder/witness.go | 2 +- core/txbuilder/witness_test.go | 9 +-- protocol/bc/spend.go | 124 ++++++++++++++++++++++++++++-- protocol/bc/transaction.go | 2 +- protocol/bc/transaction_test.go | 34 ++++---- protocol/bc/txhashes.go | 21 ++++- protocol/bc/txinput.go | 21 ++--- protocol/tx/map.go | 7 +- protocol/tx/transaction.go | 31 +++++++- protocol/tx/tx_test.go | 6 +- protocol/validation/tx.go | 16 ++-- protocol/validation/tx_test.go | 76 ++++++++++-------- protocol/vm/crypto_test.go | 10 +-- protocol/vm/introspection_test.go | 11 ++- protocol/vm/vm_test.go | 8 +- 27 files changed, 360 insertions(+), 154 deletions(-) diff --git a/core/account/builder.go b/core/account/builder.go index 7140113c87..266b761ea7 100644 --- a/core/account/builder.go +++ b/core/account/builder.go @@ -151,7 +151,7 @@ func utxoToInputs(ctx context.Context, account *signers.Signer, u *utxo, refData *txbuilder.SigningInstruction, error, ) { - txInput := bc.NewSpendInput(u.OutputID, nil, u.AssetID, u.Amount, u.ControlProgram, refData) + txInput := bc.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram, u.RefDataHash, refData) sigInst := &txbuilder.SigningInstruction{ AssetAmount: u.AssetAmount, diff --git a/core/account/builder_test.go b/core/account/builder_test.go index 41f0536c37..19f3f91caa 100644 --- a/core/account/builder_test.go +++ b/core/account/builder_test.go @@ -33,7 +33,7 @@ func TestAccountSourceReserve(t *testing.T) { accID = coretest.CreateAccount(ctx, t, accounts, "", nil) asset = coretest.CreateAsset(ctx, t, assets, nil, "", nil) - txOut, outputID = coretest.IssueAssets(ctx, t, c, g, assets, accounts, asset, 2, accID) + txOut, txRes, _ = coretest.IssueAssets(ctx, t, c, g, assets, accounts, asset, 2, accID) ) coretest.CreatePins(ctx, t, pinStore) @@ -60,7 +60,7 @@ func TestAccountSourceReserve(t *testing.T) { t.Fatal(err) } - wantTxIns := []*bc.TxInput{bc.NewSpendInput(outputID, nil, txOut.AssetID, txOut.Amount, txOut.ControlProgram, nil)} + wantTxIns := []*bc.TxInput{bc.NewSpendInput(nil, txRes.SourceID, txOut.AssetID, txOut.Amount, txRes.SourcePos, txOut.ControlProgram, txRes.RefDataHash, nil)} if !testutil.DeepEqual(tx.Inputs, wantTxIns) { t.Errorf("build txins\ngot:\n\t%+v\nwant:\n\t%+v", tx.Inputs, wantTxIns) } @@ -86,9 +86,9 @@ func TestAccountSourceUTXOReserve(t *testing.T) { assets = asset.NewRegistry(db, c, pinStore) indexer = query.NewIndexer(db, c, pinStore) - accID = coretest.CreateAccount(ctx, t, accounts, "", nil) - asset = coretest.CreateAsset(ctx, t, assets, nil, "", nil) - txOut, outputID = coretest.IssueAssets(ctx, t, c, g, assets, accounts, asset, 2, accID) + accID = coretest.CreateAccount(ctx, t, accounts, "", nil) + asset = coretest.CreateAsset(ctx, t, assets, nil, "", nil) + txOut, txRes, outputID = coretest.IssueAssets(ctx, t, c, g, assets, accounts, asset, 2, accID) ) coretest.CreatePins(ctx, t, pinStore) @@ -111,7 +111,7 @@ func TestAccountSourceUTXOReserve(t *testing.T) { t.Fatal(err) } - wantTxIns := []*bc.TxInput{bc.NewSpendInput(outputID, nil, txOut.AssetID, txOut.Amount, txOut.ControlProgram, nil)} + wantTxIns := []*bc.TxInput{bc.NewSpendInput(nil, txRes.SourceID, txOut.AssetID, txOut.Amount, txRes.SourcePos, txOut.ControlProgram, txRes.RefDataHash, nil)} if !testutil.DeepEqual(tx.Inputs, wantTxIns) { t.Errorf("build txins\ngot:\n\t%+v\nwant:\n\t%+v", tx.Inputs, wantTxIns) diff --git a/core/account/indexer.go b/core/account/indexer.go index a80e5ee2d2..e8f85529cf 100644 --- a/core/account/indexer.go +++ b/core/account/indexer.go @@ -82,6 +82,9 @@ type rawOutput struct { ControlProgram []byte txHash bc.Hash outputIndex uint32 + sourceID bc.Hash + sourcePos uint64 + refData bc.Hash } type accountOutput struct { @@ -121,6 +124,9 @@ func (m *Manager) indexAccountUTXOs(ctx context.Context, b *bc.Block) error { ControlProgram: out.ControlProgram, txHash: tx.ID, outputIndex: uint32(j), + sourceID: tx.Results[j].SourceID, + sourcePos: tx.Results[j].SourcePos, + refData: tx.Results[j].RefDataHash, } outs = append(outs, out) } @@ -147,12 +153,11 @@ func (m *Manager) indexAccountUTXOs(ctx context.Context, b *bc.Block) error { func prevoutDBKeys(txs ...*bc.Tx) (outputIDs pq.ByteaArray) { for _, tx := range txs { - for _, in := range tx.Inputs { + for i, in := range tx.Inputs { if in.IsIssuance() { continue } - o := in.SpentOutputID() - outputIDs = append(outputIDs, o.Bytes()) + outputIDs = append(outputIDs, tx.SpentOutputIDs[i].Bytes()) } } return @@ -208,6 +213,9 @@ func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*acc accountID pq.StringArray cpIndex pq.Int64Array program pq.ByteaArray + sourceID pq.ByteaArray + sourcePos pq.Int64Array + refData pq.ByteaArray ) for _, out := range outs { outputID = append(outputID, out.OutputID.Bytes()) @@ -216,13 +224,17 @@ func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*acc accountID = append(accountID, out.AccountID) cpIndex = append(cpIndex, int64(out.keyIndex)) program = append(program, out.ControlProgram) + sourceID = append(sourceID, out.sourceID[:]) + sourcePos = append(sourcePos, int64(out.sourcePos)) + refData = append(refData, out.refData[:]) } const q = ` INSERT INTO account_utxos (output_id, asset_id, amount, account_id, control_program_index, - control_program, confirmed_in) + control_program, confirmed_in, source_id, source_pos, ref_data_hash) SELECT unnest($1::bytea[]), unnest($2::bytea[]), unnest($3::bigint[]), - unnest($4::text[]), unnest($5::bigint[]), unnest($6::bytea[]), $7 + unnest($4::text[]), unnest($5::bigint[]), unnest($6::bytea[]), $7, + unnest($8::bytea[]), unnest($9::bigint[]), unnest($10::bytea[]) ON CONFLICT (output_id) DO NOTHING ` _, err := m.db.Exec(ctx, q, @@ -233,6 +245,9 @@ func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*acc cpIndex, program, block.Height, + sourceID, + sourcePos, + refData, ) return errors.Wrap(err) } diff --git a/core/account/indexer_test.go b/core/account/indexer_test.go index 3662b46120..fdc99c5d65 100644 --- a/core/account/indexer_test.go +++ b/core/account/indexer_test.go @@ -61,7 +61,7 @@ func TestDeleteUTXOs(t *testing.T) { block2 := &bc.Block{Transactions: []*bc.Tx{ bc.NewTx(bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx.OutputID(0), nil, assetID, 1, nil, nil), + bc.NewSpendInput(nil, tx.Results[0].SourceID, assetID, 1, tx.Results[0].SourcePos, acp, tx.Results[0].RefDataHash, nil), }, }), }} diff --git a/core/account/reserve.go b/core/account/reserve.go index 41f8a4a4d3..a02e063b9e 100644 --- a/core/account/reserve.go +++ b/core/account/reserve.go @@ -35,8 +35,11 @@ var ( // utxo describes an individual account utxo. type utxo struct { OutputID bc.Hash + SourceID bc.Hash bc.AssetAmount + SourcePos uint64 ControlProgram []byte + RefDataHash bc.Hash AccountID string ControlProgramIndex uint64 @@ -380,20 +383,24 @@ func (sr *sourceReserver) refillCache(ctx context.Context) error { func findMatchingUTXOs(ctx context.Context, db pg.DB, src source, height uint64) ([]*utxo, error) { const q = ` - SELECT output_id, amount, control_program_index, control_program + SELECT output_id, amount, control_program_index, control_program, + source_id, source_pos, ref_data_hash FROM account_utxos WHERE account_id = $1 AND asset_id = $2 AND confirmed_in > $3 ` var utxos []*utxo err := pg.ForQueryRows(ctx, db, q, src.AccountID, src.AssetID, height, - func(oid bc.Hash, amount uint64, cpIndex uint64, controlProg []byte) { + func(oid bc.Hash, amount uint64, cpIndex uint64, controlProg []byte, sourceID bc.Hash, sourcePos uint64, refData bc.Hash) { utxos = append(utxos, &utxo{ OutputID: oid, + SourceID: sourceID, AssetAmount: bc.AssetAmount{ Amount: amount, AssetID: src.AssetID, }, + SourcePos: sourcePos, ControlProgram: controlProg, + RefDataHash: refData, AccountID: src.AccountID, ControlProgramIndex: cpIndex, }) @@ -406,13 +413,23 @@ func findMatchingUTXOs(ctx context.Context, db pg.DB, src source, height uint64) func findSpecificUTXO(ctx context.Context, db pg.DB, out bc.Hash) (*utxo, error) { const q = ` - SELECT account_id, asset_id, amount, control_program_index, control_program + SELECT account_id, asset_id, amount, control_program_index, control_program, + source_id, source_pos, ref_data_hash FROM account_utxos WHERE output_id = $1 ` u := new(utxo) // TODO(oleg): maybe we need to scan txid:index too from here... - err := db.QueryRow(ctx, q, out).Scan(&u.AccountID, &u.AssetID, &u.Amount, &u.ControlProgramIndex, &u.ControlProgram) + err := db.QueryRow(ctx, q, out).Scan( + &u.AccountID, + &u.AssetID, + &u.Amount, + &u.ControlProgramIndex, + &u.ControlProgram, + &u.SourceID, + &u.SourcePos, + &u.RefDataHash, + ) if err == sql.ErrNoRows { return nil, pg.ErrUserInputNotFound } else if err != nil { diff --git a/core/account/reserve_test.go b/core/account/reserve_test.go index 1f95b0f935..e1e1212c1d 100644 --- a/core/account/reserve_test.go +++ b/core/account/reserve_test.go @@ -14,10 +14,12 @@ import ( const sampleAccountUTXOs = ` INSERT INTO account_utxos (output_id, asset_id, amount, account_id, control_program_index, - control_program, confirmed_in) VALUES ( + control_program, confirmed_in, source_id, source_pos, ref_data_hash) VALUES ( decode('9886ae2dc24b6d868c68768038c43801e905a62f1a9b826ca0dc357f00c30117', 'hex'), decode('df1df9d4f66437ab5be715e4d1faeb29d24c80a6dc8276d6a630f05c5f1f7693', 'hex'), - 1000, 'accEXAMPLE', 1, '\x6a'::bytea, 1); + 1000, 'accEXAMPLE', 1, '\x6a'::bytea, 1, + decode('905a62f1a9b826ca0dc357f00c301179886ae2dc24b6d868c68768038c43801e', 'hex'), + 0, decode('0000000000000000000000000000000000000000000000000000000000000000', 'hex')); ` func TestCancelReservation(t *testing.T) { diff --git a/core/coretest/fixtures.go b/core/coretest/fixtures.go index 48cb0b4c5a..3c2f33773b 100644 --- a/core/coretest/fixtures.go +++ b/core/coretest/fixtures.go @@ -45,7 +45,7 @@ func CreateAsset(ctx context.Context, t testing.TB, assets *asset.Registry, def return asset.AssetID } -func IssueAssets(ctx context.Context, t testing.TB, c *protocol.Chain, s txbuilder.Submitter, assets *asset.Registry, accounts *account.Manager, assetID bc.AssetID, amount uint64, accountID string) (*bc.TxOutput, bc.Hash) { +func IssueAssets(ctx context.Context, t testing.TB, c *protocol.Chain, s txbuilder.Submitter, assets *asset.Registry, accounts *account.Manager, assetID bc.AssetID, amount uint64, accountID string) (*bc.TxOutput, *bc.ResultInfo, bc.Hash) { assetAmount := bc.AssetAmount{AssetID: assetID, Amount: amount} tpl, err := txbuilder.Build(ctx, nil, []txbuilder.Action{ @@ -63,7 +63,7 @@ func IssueAssets(ctx context.Context, t testing.TB, c *protocol.Chain, s txbuild testutil.FatalErr(t, err) } - return tpl.Transaction.Outputs[0], tpl.Transaction.OutputID(0) + return tpl.Transaction.Outputs[0], &tpl.Transaction.Results[0], tpl.Transaction.OutputID(0) } func Transfer(ctx context.Context, t testing.TB, c *protocol.Chain, s txbuilder.Submitter, actions []txbuilder.Action) *bc.Tx { diff --git a/core/migrate/data.go b/core/migrate/data.go index 24fec87c6c..5a95d3a127 100644 --- a/core/migrate/data.go +++ b/core/migrate/data.go @@ -42,4 +42,10 @@ var migrations = []migration{ DROP COLUMN index; ALTER TABLE account_utxos ADD PRIMARY KEY (output_id); `}, + {Name: `2017-03-02.0.core.add-output-source-info.sql`, SQL: ` + ALTER TABLE account_utxos + ADD COLUMN source_id bytea NOT NULL, + ADD COLUMN source_pos bigint NOT NULL, + ADD COLUMN ref_data_hash bytea NOT NULL; + `}, } diff --git a/core/query/annotated.go b/core/query/annotated.go index 0ee1a77906..513d6cb62c 100644 --- a/core/query/annotated.go +++ b/core/query/annotated.go @@ -130,8 +130,8 @@ func buildAnnotatedTransaction(orig *bc.Tx, b *bc.Block, indexInBlock uint32) *A tx.ReferenceData = &referenceData } - for _, in := range orig.Inputs { - tx.Inputs = append(tx.Inputs, buildAnnotatedInput(in)) + for i := range orig.Inputs { + tx.Inputs = append(tx.Inputs, buildAnnotatedInput(orig, uint32(i))) } for i := range orig.Outputs { tx.Outputs = append(tx.Outputs, buildAnnotatedOutput(orig, uint32(i))) @@ -139,7 +139,8 @@ func buildAnnotatedTransaction(orig *bc.Tx, b *bc.Block, indexInBlock uint32) *A return tx } -func buildAnnotatedInput(orig *bc.TxInput) *AnnotatedInput { +func buildAnnotatedInput(tx *bc.Tx, i uint32) *AnnotatedInput { + orig := tx.Inputs[i] in := &AnnotatedInput{ AssetID: orig.AssetID(), Amount: orig.Amount(), @@ -158,8 +159,7 @@ func buildAnnotatedInput(orig *bc.TxInput) *AnnotatedInput { in.Type = "issue" in.IssuanceProgram = prog } else { - prevoutID := orig.SpentOutputID() - in.Type = "spend" + prevoutID := tx.SpentOutputIDs[i] in.ControlProgram = orig.ControlProgram() in.SpentOutputID = &prevoutID } diff --git a/core/query/index.go b/core/query/index.go index 988f7429a8..576fd6ef59 100644 --- a/core/query/index.go +++ b/core/query/index.go @@ -205,10 +205,9 @@ func (ind *Indexer) insertAnnotatedOutputs(ctx context.Context, b *bc.Block, ann prevoutIDs pq.ByteaArray ) for pos, tx := range b.Transactions { - for _, in := range tx.Inputs { + for i, in := range tx.Inputs { if !in.IsIssuance() { - prevoutID := in.SpentOutputID() - prevoutIDs = append(prevoutIDs, prevoutID.Bytes()) + prevoutIDs = append(prevoutIDs, tx.SpentOutputIDs[i].Bytes()) } } diff --git a/core/schema.sql b/core/schema.sql index e4e6573a39..056f7f3fbd 100644 --- a/core/schema.sql +++ b/core/schema.sql @@ -194,7 +194,10 @@ CREATE TABLE account_utxos ( control_program_index bigint NOT NULL, control_program bytea NOT NULL, confirmed_in bigint NOT NULL, - output_id bytea NOT NULL + output_id bytea NOT NULL, + source_id bytea NOT NULL, + source_pos bigint NOT NULL, + ref_data_hash bytea NOT NULL ); @@ -895,3 +898,4 @@ insert into migrations (filename, hash) values ('2017-02-03.0.core.schema-snapsh insert into migrations (filename, hash) values ('2017-02-07.0.query.non-null-alias.sql', '17028a0bdbc95911e299dc65fe641184e54c87a0d07b3c576d62d023b9a8defc'); insert into migrations (filename, hash) values ('2017-02-16.0.query.spent-output.sql', '7cd52095b6f202d7a25ffe666b7b7d60e7700d314a7559b911e236b72661a738'); insert into migrations (filename, hash) values ('2017-02-28.0.core.remove-outpoints.sql', '067638e2a826eac70d548f2d6bb234660f3200064072baf42db741456ecf8deb'); +insert into migrations (filename, hash) values ('2017-03-02.0.core.add-output-source-info.sql', 'f44c7cfbff346f6f797d497910c0a76f2a7600ca8b5be4fe4e4a04feaf32e0df'); diff --git a/core/txbuilder/txbuilder_test.go b/core/txbuilder/txbuilder_test.go index 52ee0c8a8e..b9b3e591ee 100644 --- a/core/txbuilder/txbuilder_test.go +++ b/core/txbuilder/txbuilder_test.go @@ -23,7 +23,7 @@ import ( type testAction bc.AssetAmount func (t testAction) Build(ctx context.Context, b *TemplateBuilder) error { - in := bc.NewSpendInput(bc.Hash{255}, nil, t.AssetID, t.Amount, nil, nil) + in := bc.NewSpendInput(nil, bc.Hash{255}, t.AssetID, t.Amount, 0, nil, bc.Hash{}, nil) tplIn := &SigningInstruction{} err := b.AddInput(in, tplIn) @@ -59,7 +59,7 @@ func TestBuild(t *testing.T) { Version: 1, MaxTime: bc.Millis(expiryTime), Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{255}, nil, [32]byte{1}, 5, nil, nil), + bc.NewSpendInput(nil, bc.Hash{255}, [32]byte{1}, 5, 0, nil, bc.Hash{}, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput([32]byte{2}, 6, []byte("dest"), nil), @@ -321,8 +321,7 @@ func TestTxSighashCommitment(t *testing.T) { tx.Inputs = append(tx.Inputs, &bc.TxInput{ AssetVersion: 1, TypedInput: &bc.SpendInput{ - SpentOutputID: bc.Hash{1}, - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ AssetID: assetID, Amount: 2, @@ -341,8 +340,7 @@ func TestTxSighashCommitment(t *testing.T) { // Tx with a spend input committing to the wrong txsighash is not OK spendInput := &bc.SpendInput{ - SpentOutputID: bc.Hash{2}, - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ AssetID: assetID, Amount: 3, @@ -370,8 +368,7 @@ func TestTxSighashCommitment(t *testing.T) { // Tx with a spend input committing to the right txsighash is OK spendInput = &bc.SpendInput{ - SpentOutputID: bc.Hash{3}, - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ AssetID: assetID, Amount: 4, @@ -400,7 +397,7 @@ func TestTxSighashCommitment(t *testing.T) { //Tx with a spend input missing signature argument is not OK spendInput = &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ AssetID: assetID, Amount: 5, @@ -434,27 +431,27 @@ func TestCheckBlankCheck(t *testing.T) { want error }{{ tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil)}, }, want: ErrBlankCheck, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil)}, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{0}, 3, nil, nil)}, }, want: ErrBlankCheck, }, { tx: &bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil), - bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{1}, 5, nil, nil), + bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil), + bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{1}, 5, 0, nil, bc.Hash{}, nil), }, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{0}, 5, nil, nil)}, }, want: ErrBlankCheck, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil)}, Outputs: []*bc.TxOutput{ bc.NewTxOutput(bc.AssetID{0}, math.MaxInt64, nil, nil), bc.NewTxOutput(bc.AssetID{0}, 7, nil, nil), @@ -464,14 +461,14 @@ func TestCheckBlankCheck(t *testing.T) { }, { tx: &bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil), - bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, math.MaxInt64, nil, nil), + bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil), + bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, math.MaxInt64, 0, nil, bc.Hash{}, nil), }, }, want: ErrBadAmount, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil)}, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{0}, 5, nil, nil)}, }, want: nil, @@ -482,7 +479,7 @@ func TestCheckBlankCheck(t *testing.T) { want: nil, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{255}, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(nil, bc.Hash{255}, bc.AssetID{0}, 5, 0, nil, bc.Hash{}, nil)}, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{1}, 5, nil, nil)}, }, want: nil, diff --git a/core/txbuilder/witness.go b/core/txbuilder/witness.go index 444881a814..a715aeaa4e 100644 --- a/core/txbuilder/witness.go +++ b/core/txbuilder/witness.go @@ -167,7 +167,7 @@ func buildSigProgram(tpl *Template, index uint32) []byte { }) inp := tpl.Transaction.Inputs[index] if !inp.IsIssuance() { - constraints = append(constraints, outputIDConstraint(inp.SpentOutputID())) + constraints = append(constraints, outputIDConstraint(tpl.Transaction.SpentOutputIDs[index])) } // Commitment to the tx-level refdata is conditional on it being diff --git a/core/txbuilder/witness_test.go b/core/txbuilder/witness_test.go index 1db839a915..538c8aa036 100644 --- a/core/txbuilder/witness_test.go +++ b/core/txbuilder/witness_test.go @@ -15,22 +15,21 @@ import ( ) func TestInferConstraints(t *testing.T) { - outputID := bc.Hash{255} tpl := &Template{ - Transaction: &bc.Tx{TxData: bc.TxData{ + Transaction: bc.NewTx(bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(outputID, nil, bc.AssetID{}, 123, nil, []byte{1}), + bc.NewSpendInput(nil, bc.Hash{}, bc.AssetID{}, 123, 0, nil, bc.Hash{}, []byte{1}), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(bc.AssetID{}, 123, []byte{10, 11, 12}, nil), }, MinTime: 1, MaxTime: 2, - }}, + }), AllowAdditional: true, } prog := buildSigProgram(tpl, 0) - wantSrc := fmt.Sprintf("MINTIME 1 GREATERTHANOREQUAL VERIFY MAXTIME 2 LESSTHANOREQUAL VERIFY 0x%x OUTPUTID EQUAL VERIFY 0x2767f15c8af2f2c7225d5273fdd683edc714110a987d1054697c348aed4e6cc7 REFDATAHASH EQUAL VERIFY 0 0 123 0x0000000000000000000000000000000000000000000000000000000000000000 1 0x0a0b0c CHECKOUTPUT", outputID[:]) + wantSrc := fmt.Sprintf("MINTIME 1 GREATERTHANOREQUAL VERIFY MAXTIME 2 LESSTHANOREQUAL VERIFY 0x%x OUTPUTID EQUAL VERIFY 0x2767f15c8af2f2c7225d5273fdd683edc714110a987d1054697c348aed4e6cc7 REFDATAHASH EQUAL VERIFY 0 0 123 0x0000000000000000000000000000000000000000000000000000000000000000 1 0x0a0b0c CHECKOUTPUT", tpl.Transaction.SpentOutputIDs[0].Bytes()) want, err := vm.Assemble(wantSrc) if err != nil { t.Fatal(err) diff --git a/protocol/bc/spend.go b/protocol/bc/spend.go index 272ac01f2e..5a52083b2e 100644 --- a/protocol/bc/spend.go +++ b/protocol/bc/spend.go @@ -1,13 +1,21 @@ package bc +import ( + "fmt" + "io" + + "chain/crypto/sha3pool" + "chain/encoding/blockchain" + "chain/errors" +) + // SpendInput satisfies the TypedInput interface and represents a spend transaction. type SpendInput struct { // Commitment - SpentOutputID Hash - OutputCommitment + SpendCommitment // The unconsumed suffix of the output commitment - OutputCommitmentSuffix []byte + SpendCommitmentSuffix []byte // Witness Arguments [][]byte @@ -15,26 +23,126 @@ type SpendInput struct { func (si *SpendInput) IsIssuance() bool { return false } -func NewSpendInput(prevoutID Hash, arguments [][]byte, assetID AssetID, amount uint64, controlProgram, referenceData []byte) *TxInput { +func NewSpendInput(arguments [][]byte, sourceID Hash, assetID AssetID, amount uint64, sourcePos uint64, controlProgram []byte, outRefDataHash Hash, referenceData []byte) *TxInput { const ( vmver = 1 assetver = 1 ) - oc := OutputCommitment{ + sc := SpendCommitment{ AssetAmount: AssetAmount{ AssetID: assetID, Amount: amount, }, + SourceID: sourceID, + SourcePosition: sourcePos, VMVersion: vmver, ControlProgram: controlProgram, + RefDataHash: outRefDataHash, } return &TxInput{ AssetVersion: assetver, ReferenceData: referenceData, TypedInput: &SpendInput{ - SpentOutputID: prevoutID, - OutputCommitment: oc, - Arguments: arguments, + SpendCommitment: sc, + Arguments: arguments, }, } } + +// SpendCommitment contains the commitment data for a transaction +// output (which also appears in the spend input of that output). +type SpendCommitment struct { + AssetAmount + SourceID Hash + SourcePosition uint64 + VMVersion uint64 + ControlProgram []byte + RefDataHash Hash +} + +func (sc *SpendCommitment) writeExtensibleString(w io.Writer, suffix []byte, assetVersion uint64) error { + _, err := blockchain.WriteExtensibleString(w, suffix, func(w io.Writer) error { + return sc.writeContents(w, suffix, assetVersion) + }) + return err +} + +func (sc *SpendCommitment) writeContents(w io.Writer, suffix []byte, assetVersion uint64) (err error) { + if assetVersion == 1 { + _, err = sc.SourceID.WriteTo(w) + if err != nil { + return errors.Wrap(err, "writing source id") + } + err = sc.AssetAmount.writeTo(w) + if err != nil { + return errors.Wrap(err, "writing asset amount") + } + _, err = blockchain.WriteVarint63(w, sc.SourcePosition) + if err != nil { + return errors.Wrap(err, "writing source position") + } + _, err = blockchain.WriteVarint63(w, sc.VMVersion) + if err != nil { + return errors.Wrap(err, "writing vm version") + } + _, err = blockchain.WriteVarstr31(w, sc.ControlProgram) + if err != nil { + return errors.Wrap(err, "writing control program") + } + _, err = sc.RefDataHash.WriteTo(w) + if err != nil { + return errors.Wrap(err, "writing reference data hash") + } + } + if len(suffix) > 0 { + _, err = w.Write(suffix) + if err != nil { + return errors.Wrap(err, "writing suffix") + } + } + return nil +} + +func (sc *SpendCommitment) readFrom(r io.Reader, assetVersion uint64) (suffix []byte, n int, err error) { + return blockchain.ReadExtensibleString(r, func(r io.Reader) error { + if assetVersion == 1 { + _, err := sc.SourceID.readFrom(r) + if err != nil { + return errors.Wrap(err, "reading source id") + } + _, err = sc.AssetAmount.readFrom(r) + if err != nil { + return errors.Wrap(err, "reading asset+amount") + } + sc.SourcePosition, _, err = blockchain.ReadVarint63(r) + if err != nil { + return errors.Wrap(err, "reading source position") + } + sc.VMVersion, _, err = blockchain.ReadVarint63(r) + if err != nil { + return errors.Wrap(err, "reading VM version") + } + if sc.VMVersion != 1 { + return fmt.Errorf("unrecognized VM version %d for asset version 1", sc.VMVersion) + } + sc.ControlProgram, _, err = blockchain.ReadVarstr31(r) + if err != nil { + return errors.Wrap(err, "reading control program") + } + _, err = sc.RefDataHash.readFrom(r) + if err != nil { + return errors.Wrap(err, "reading reference data hash") + } + return nil + } + return nil + }) +} + +func (sc *SpendCommitment) Hash(suffix []byte, assetVersion uint64) (spendhash Hash) { + h := sha3pool.Get256() + defer sha3pool.Put256(h) + sc.writeExtensibleString(h, suffix, assetVersion) // TODO(oleg): get rid of this assetVersion parameter to actually write all the bytes + h.Read(spendhash[:]) + return spendhash +} diff --git a/protocol/bc/transaction.go b/protocol/bc/transaction.go index 61f7893316..5ae460385e 100644 --- a/protocol/bc/transaction.go +++ b/protocol/bc/transaction.go @@ -221,7 +221,7 @@ func (tx *TxData) IssuanceHash(n int) (h Hash, err error) { } func (tx *Tx) OutputID(outputIndex uint32) Hash { - return tx.ResultHashes[outputIndex] + return tx.Results[outputIndex].ID } func (tx *TxData) MarshalText() ([]byte, error) { diff --git a/protocol/bc/transaction_test.go b/protocol/bc/transaction_test.go index dd6dcfbeff..3516955224 100644 --- a/protocol/bc/transaction_test.go +++ b/protocol/bc/transaction_test.go @@ -98,7 +98,7 @@ func TestTransaction(t *testing.T) { tx: NewTx(TxData{ Version: 1, Inputs: []*TxInput{ - NewSpendInput(mustDecodeHash("dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292"), nil, AssetID{}, 1000000000000, []byte{1}, []byte("input")), + NewSpendInput(nil, mustDecodeHash("dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292"), AssetID{}, 1000000000000, 1, []byte{1}, Hash{}, []byte("input")), }, Outputs: []*TxOutput{ NewTxOutput(ComputeAssetID(issuanceScript, initialBlockHash, 1, EmptyStringHash), 600000000000, []byte{1}, nil), @@ -117,14 +117,16 @@ func TestTransaction(t *testing.T) { "00" + // common witness extensible string length "01" + // inputs count "01" + // input 0, asset version - "4b" + // input 0, input commitment length prefix - "01" + // input 0, input commitment, "spend" type - "dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292" + // input 0, spend input commitment, output ID - "29" + // input 0, spend input commitment, output commitment length prefix - "0000000000000000000000000000000000000000000000000000000000000000" + // input 0, spend input commitment, output commitment, asset id - "80a094a58d1d" + // input 0, spend input commitment, output commitment, amount - "01" + // input 0, spend input commitment, output commitment, vm version - "0101" + // input 0, spend input commitment, output commitment, control program + "6c" + // input 0, input commitment length prefix + "01" + // input 0, input commitment, "spend" type+ + "6a" + // input 0, spend input commitment, spend commitment length prefix + "dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292" + // input 0, spend input commitment, spend commitment, source ID + "0000000000000000000000000000000000000000000000000000000000000000" + // input 0, spend input commitment, spend commitment, asset id + "80a094a58d1d" + // input 0, spend input commitment, spend commitment, amount + "01" + // input 0, spend input commitment, spend commitment, source position + "01" + // input 0, spend input commitment, spend commitment, vm version + "0101" + // input 0, spend input commitment, spend commitment, control program + "0000000000000000000000000000000000000000000000000000000000000000" + // input 0, spend input commitment, spend commitment, reference data hash "05696e707574" + // input 0, reference data "01" + // input 0, input witness length prefix "00" + // input 0, input witness, number of args @@ -146,7 +148,7 @@ func TestTransaction(t *testing.T) { "00" + // output 1, reference data "00" + // output 1, output witness "0c646973747269627574696f6e"), // reference data - hash: mustDecodeHash("db56dd46919fe91055e6441bdee2c7e0393f561995e11bd2a063b690adde4205"), + hash: mustDecodeHash("c328ad4278045b4c50e8af7e7d0df198e7d9436d2b5de35df1339f13a1192331"), }, //07 @@ -232,7 +234,7 @@ func TestHasIssuance(t *testing.T) { }, { tx: &TxData{ Inputs: []*TxInput{ - NewSpendInput(Hash{}, nil, AssetID{}, 0, nil, nil), + NewSpendInput(nil, Hash{}, AssetID{}, 0, 0, nil, Hash{}, nil), NewIssuanceInput(nil, 0, nil, Hash{}, nil, nil, nil), }, }, @@ -240,7 +242,7 @@ func TestHasIssuance(t *testing.T) { }, { tx: &TxData{ Inputs: []*TxInput{ - NewSpendInput(Hash{}, nil, AssetID{}, 0, nil, nil), + NewSpendInput(nil, Hash{}, AssetID{}, 0, 0, nil, Hash{}, nil), }, }, want: false, @@ -316,7 +318,7 @@ func BenchmarkTxWriteToFalse(b *testing.B) { func BenchmarkTxWriteToTrue200(b *testing.B) { tx := &Tx{} for i := 0; i < 200; i++ { - tx.Inputs = append(tx.Inputs, NewSpendInput(Hash{}, nil, AssetID{}, 0, nil, nil)) + tx.Inputs = append(tx.Inputs, NewSpendInput(nil, Hash{}, AssetID{}, 0, 0, nil, Hash{}, nil)) tx.Outputs = append(tx.Outputs, NewTxOutput(AssetID{}, 0, nil, nil)) } for i := 0; i < b.N; i++ { @@ -327,7 +329,7 @@ func BenchmarkTxWriteToTrue200(b *testing.B) { func BenchmarkTxWriteToFalse200(b *testing.B) { tx := &Tx{} for i := 0; i < 200; i++ { - tx.Inputs = append(tx.Inputs, NewSpendInput(Hash{}, nil, AssetID{}, 0, nil, nil)) + tx.Inputs = append(tx.Inputs, NewSpendInput(nil, Hash{}, AssetID{}, 0, 0, nil, Hash{}, nil)) tx.Outputs = append(tx.Outputs, NewTxOutput(AssetID{}, 0, nil, nil)) } for i := 0; i < b.N; i++ { @@ -336,7 +338,7 @@ func BenchmarkTxWriteToFalse200(b *testing.B) { } func BenchmarkTxInputWriteToTrue(b *testing.B) { - input := NewSpendInput(Hash{}, nil, AssetID{}, 0, nil, nil) + input := NewSpendInput(nil, Hash{}, AssetID{}, 0, 0, nil, Hash{}, nil) ew := errors.NewWriter(ioutil.Discard) for i := 0; i < b.N; i++ { input.writeTo(ew, 0) @@ -344,7 +346,7 @@ func BenchmarkTxInputWriteToTrue(b *testing.B) { } func BenchmarkTxInputWriteToFalse(b *testing.B) { - input := NewSpendInput(Hash{}, nil, AssetID{}, 0, nil, nil) + input := NewSpendInput(nil, Hash{}, AssetID{}, 0, 0, nil, Hash{}, nil) ew := errors.NewWriter(ioutil.Discard) for i := 0; i < b.N; i++ { input.writeTo(ew, serRequired) diff --git a/protocol/bc/txhashes.go b/protocol/bc/txhashes.go index 41b7de6f4c..614fe29032 100644 --- a/protocol/bc/txhashes.go +++ b/protocol/bc/txhashes.go @@ -4,14 +4,25 @@ type ( // TxHashes holds data needed for validation and state updates. TxHashes struct { ID Hash + // contains OutputIDs and retirement hashes. - // each OutputID is also the corresponding UnspentID - ResultHashes []Hash - Issuances []struct { + Results []ResultInfo + Issuances []struct { ID Hash ExpirationMS uint64 } - VMContexts []*VMContext // one per old-style Input + SpentOutputIDs []Hash + VMContexts []*VMContext // one per old-style Input + } + + // ResultInfo contains information about each result in a transaction header. + ResultInfo struct { + ID Hash // outputID + + // The following fields apply only to results that are outputs (not retirements). + SourceID Hash // the ID of this output's source entry + SourcePos uint64 // the position within the source entry of this output's value + RefDataHash Hash // contents of the result entry's data field (which is a hash of the source refdata, when converting from old-style transactions) } VMContext struct { @@ -40,3 +51,5 @@ func (t TxHashes) SigHash(n uint32) Hash { // that can compute the hash of a blockheader. It is a variable here // to avoid a circular dependency between the bc and tx packages. var BlockHeaderHashFunc func(*BlockHeader) Hash + +var OutputHash func(*SpendCommitment) (Hash, error) diff --git a/protocol/bc/txinput.go b/protocol/bc/txinput.go index 4ec4e40897..853c625a1f 100644 --- a/protocol/bc/txinput.go +++ b/protocol/bc/txinput.go @@ -125,12 +125,7 @@ func (t *TxInput) readFrom(r io.Reader) (err error) { case 1: si = new(SpendInput) - - _, err = si.SpentOutputID.readFrom(r) - if err != nil { - return err - } - si.OutputCommitmentSuffix, _, err = si.OutputCommitment.readFrom(r, 1) + si.SpendCommitmentSuffix, _, err = si.SpendCommitment.readFrom(r, 1) if err != nil { return err } @@ -255,14 +250,10 @@ func (t *TxInput) WriteInputCommitment(w io.Writer, serflags uint8) error { if err != nil { return err } - _, err = inp.SpentOutputID.WriteTo(w) - if err != nil { - return err - } if serflags&SerPrevout != 0 { - err = inp.OutputCommitment.writeExtensibleString(w, inp.OutputCommitmentSuffix, t.AssetVersion) + err = inp.SpendCommitment.writeExtensibleString(w, inp.SpendCommitmentSuffix, t.AssetVersion) } else { - prevouthash := inp.OutputCommitment.Hash(inp.OutputCommitmentSuffix, t.AssetVersion) + prevouthash := inp.SpendCommitment.Hash(inp.SpendCommitmentSuffix, t.AssetVersion) _, err = w.Write(prevouthash[:]) } return err @@ -302,9 +293,9 @@ func (t *TxInput) writeInputWitness(w io.Writer) error { return nil } -func (t *TxInput) SpentOutputID() (o Hash) { +func (t *TxInput) SpentOutputID() (o Hash, err error) { if si, ok := t.TypedInput.(*SpendInput); ok { - o = si.SpentOutputID + o, err = OutputHash(&si.SpendCommitment) } - return o + return o, err } diff --git a/protocol/tx/map.go b/protocol/tx/map.go index da7f41bf68..c2e1682ce5 100644 --- a/protocol/tx/map.go +++ b/protocol/tx/map.go @@ -33,8 +33,13 @@ func mapTx(tx *bc.TxData) (headerID bc.Hash, hdr *header, entryMap map[bc.Hash]e for i, inp := range tx.Inputs { if oldSp, ok := inp.TypedInput.(*bc.SpendInput); ok { + var oldSpID bc.Hash + oldSpID, err = ComputeOutputID(&oldSp.SpendCommitment) + if err != nil { + return + } var spID bc.Hash - spID, _, err = addEntry(newSpend(oldSp.SpentOutputID, hashData(inp.ReferenceData), i)) + spID, _, err = addEntry(newSpend(oldSpID, hashData(inp.ReferenceData), i)) if err != nil { err = errors.Wrapf(err, "adding spend entry for input %d", i) return diff --git a/protocol/tx/transaction.go b/protocol/tx/transaction.go index 6de085883d..c536cbb82c 100644 --- a/protocol/tx/transaction.go +++ b/protocol/tx/transaction.go @@ -14,6 +14,23 @@ func init() { hash, _ := mapBlockHeader(old) return hash } + bc.OutputHash = ComputeOutputID +} + +func ComputeOutputID(sc *bc.SpendCommitment) (h bc.Hash, err error) { + defer func() { + if r, ok := recover().(error); ok { + err = r + } + }() + o := newOutput(valueSource{ + Ref: sc.SourceID, + Value: sc.AssetAmount, + Position: sc.SourcePosition, + }, program{VMVersion: sc.VMVersion, Code: sc.ControlProgram}, sc.RefDataHash, 0) + + h = entryID(o) + return h, nil } // TxHashes returns all hashes needed for validation and state updates. @@ -26,13 +43,20 @@ func TxHashes(oldTx *bc.TxData) (hashes *bc.TxHashes, err error) { hashes = new(bc.TxHashes) hashes.ID = bc.Hash(txid) - // ResultHashes - hashes.ResultHashes = make([]bc.Hash, len(header.body.Results)) + // Results + hashes.Results = make([]bc.ResultInfo, len(header.body.Results)) for i, resultHash := range header.body.Results { - hashes.ResultHashes[i] = bc.Hash(resultHash) + hashes.Results[i].ID = resultHash + entry := entries[resultHash] + if out, ok := entry.(*output); ok { + hashes.Results[i].SourceID = out.body.Source.Ref + hashes.Results[i].SourcePos = out.body.Source.Position + hashes.Results[i].RefDataHash = out.body.Data + } } hashes.VMContexts = make([]*bc.VMContext, len(oldTx.Inputs)) + hashes.SpentOutputIDs = make([]bc.Hash, len(oldTx.Inputs)) for entryID, ent := range entries { switch ent := ent.(type) { @@ -62,6 +86,7 @@ func TxHashes(oldTx *bc.TxData) (hashes *bc.TxHashes, err error) { vmc := newVMContext(bc.Hash(entryID), hashes.ID, header.body.Data, ent.body.Data) vmc.OutputID = (*bc.Hash)(&ent.body.SpentOutput) hashes.VMContexts[ent.Ordinal()] = vmc + hashes.SpentOutputIDs[ent.Ordinal()] = ent.body.SpentOutput } } diff --git a/protocol/tx/tx_test.go b/protocol/tx/tx_test.go index 02d13a5b7b..eec8f0efd9 100644 --- a/protocol/tx/tx_test.go +++ b/protocol/tx/tx_test.go @@ -18,7 +18,7 @@ func TestTxHashes(t *testing.T) { }, { txdata: sampleTx(), - hash: mustDecodeHash("0a1ef3935e4bc83a8b59603146fc88d09405b9f5531ac5774f61350290b0fe1c"), + hash: mustDecodeHash("9fad4f5024412d99d17508ef3cc66f81f1e09914a71b2641683acca87081c098"), // todo: verify this value, }, } @@ -72,8 +72,8 @@ func sampleTx() *bc.TxData { return &bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(mustDecodeHash("dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292"), nil, assetID, 1000000000000, []byte{1}, []byte("input")), - bc.NewSpendInput(bc.Hash{17}, nil, assetID, 1, []byte{2}, []byte("input2")), + bc.NewSpendInput(nil, mustDecodeHash("dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292"), assetID, 1000000000000, 1, []byte{1}, bc.Hash{}, []byte("input")), + bc.NewSpendInput(nil, bc.Hash{17}, assetID, 1, 1, []byte{2}, bc.Hash{}, []byte("input2")), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(assetID, 600000000000, []byte{1}, nil), diff --git a/protocol/validation/tx.go b/protocol/validation/tx.go index a85b73026c..79f65ae745 100644 --- a/protocol/validation/tx.go +++ b/protocol/validation/tx.go @@ -103,9 +103,14 @@ func ConfirmTx(snapshot *state.Snapshot, initialBlockHash bc.Hash, blockVersion, // txin is a spend + spentOutputID, err := txin.SpentOutputID() + if err != nil { + return badTxErrf(errInvalidOutput, "could not compute output id for input %d", i) + } + // Lookup the prevout in the blockchain state tree. - if !snapshot.Tree.Contains(txin.SpentOutputID().Bytes()) { - return badTxErrf(errInvalidOutput, "output %s for input %d is invalid", txin.SpentOutputID().String(), i) + if !snapshot.Tree.Contains(spentOutputID.Bytes()) { + return badTxErrf(errInvalidOutput, "output %s for input %d is invalid", spentOutputID, i) } } return nil @@ -268,10 +273,11 @@ func ApplyTx(snapshot *state.Snapshot, tx *bc.Tx) error { continue } - si := in.TypedInput.(*bc.SpendInput) - // Remove the consumed output from the state tree. - uid := si.SpentOutputID + uid, err := in.SpentOutputID() + if err != nil { + return err + } snapshot.Tree.Delete(uid.Bytes()) } diff --git a/protocol/validation/tx_test.go b/protocol/validation/tx_test.go index 4ff1b811b2..9a9ea0d8d8 100644 --- a/protocol/validation/tx_test.go +++ b/protocol/validation/tx_test.go @@ -96,7 +96,7 @@ func TestUniqueIssuance(t *testing.T) { tx = bc.NewTx(bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx.OutputID(0), nil, assetID, 1, trueProg, nil), + bc.NewSpendInput(nil, tx.Results[0].SourceID, assetID, 1, tx.Results[0].SourcePos, trueProg, tx.Results[0].RefDataHash, nil), issuance2Inp, }, Outputs: []*bc.TxOutput{ @@ -168,6 +168,9 @@ func TestTxWellFormed(t *testing.T) { aid2 := bc.AssetID([32]byte{2}) tx1 := bc.NewTx(bc.TxData{ + Inputs: []*bc.TxInput{ + bc.NewIssuanceInput([]byte{1}, 10, nil, initialBlockHash, issuanceProg, nil, nil), + }, Outputs: []*bc.TxOutput{ { OutputCommitment: bc.OutputCommitment{ @@ -191,6 +194,8 @@ func TestTxWellFormed(t *testing.T) { }, }, }) + t.Log(tx1.Results[0].SourceID, tx1.Results[0].SourcePos, tx1.Results[0].RefDataHash) + t.Log(tx2.Results[0].SourceID, tx2.Results[0].SourcePos, tx2.Results[0].RefDataHash) testCases := []struct { suberr error @@ -207,7 +212,7 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx1.OutputID(0), nil, aid1, 1000, nil, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid1, 1000, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 999, nil, nil), @@ -219,8 +224,8 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx1.OutputID(0), nil, aid1, 500, nil, nil), - bc.NewSpendInput(tx2.OutputID(0), nil, aid2, 500, nil, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid1, 500, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), + bc.NewSpendInput(nil, tx2.Results[0].SourceID, aid2, 500, tx2.Results[0].SourcePos, trueProg, tx2.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 500, nil, nil), @@ -234,7 +239,7 @@ func TestTxWellFormed(t *testing.T) { Version: 1, Inputs: []*bc.TxInput{ bc.NewIssuanceInput(nil, 0, nil, initialBlockHash, issuanceProg, nil, nil), - bc.NewSpendInput(tx1.OutputID(0), nil, aid2, 0, nil, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid2, 0, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 0, nil, nil), @@ -246,7 +251,7 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx1.OutputID(0), nil, aid1, 1000, trueProg, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid1, 1000, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 1000, nil, nil), @@ -257,8 +262,8 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx1.OutputID(0), nil, aid1, 500, trueProg, nil), - bc.NewSpendInput(tx2.OutputID(0), nil, aid2, 500, trueProg, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid1, 500, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), + bc.NewSpendInput(nil, tx2.Results[0].SourceID, aid2, 500, tx2.Results[0].SourcePos, trueProg, tx2.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 500, nil, nil), @@ -272,8 +277,8 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx1.OutputID(0), nil, aid1, 500, trueProg, nil), - bc.NewSpendInput(tx2.OutputID(0), nil, aid1, 500, trueProg, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid1, 500, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), + bc.NewSpendInput(nil, tx2.Results[0].SourceID, aid1, 500, tx2.Results[0].SourcePos, trueProg, tx2.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 1000, nil, nil), @@ -287,7 +292,7 @@ func TestTxWellFormed(t *testing.T) { MinTime: 2, MaxTime: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx1.OutputID(0), nil, aid1, 1000, nil, nil), + bc.NewSpendInput(nil, tx1.Results[0].SourceID, aid1, 1000, tx1.Results[0].SourcePos, trueProg, tx1.Results[0].RefDataHash, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 1000, nil, nil), @@ -301,7 +306,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -333,7 +338,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -365,7 +370,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 2, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -397,7 +402,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -429,7 +434,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -461,7 +466,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -493,7 +498,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -526,7 +531,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 2, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -559,7 +564,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -592,7 +597,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -625,7 +630,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -658,7 +663,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -690,7 +695,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: math.MaxInt64, }, @@ -702,7 +707,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 1, }, @@ -722,7 +727,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 10, }, @@ -734,7 +739,7 @@ func TestTxWellFormed(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 10, }, @@ -780,7 +785,7 @@ func TestTxRangeErrs(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: math.MaxInt64 + 1, }, @@ -796,7 +801,7 @@ func TestTxRangeErrs(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ AssetAmount: bc.AssetAmount{ Amount: 10, }, @@ -952,6 +957,7 @@ func TestConfirmTx(t *testing.T) { }) outid1 := tx.OutputID(0) + outres := tx.Results[0] snapshot := state.Empty() err := snapshot.Tree.Insert(outid1[:]) @@ -1025,7 +1031,7 @@ func TestConfirmTx(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{}, + SpendCommitment: bc.SpendCommitment{}, }, }, }, @@ -1039,8 +1045,14 @@ func TestConfirmTx(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - SpentOutputID: outid1, - OutputCommitment: out1, + SpendCommitment: bc.SpendCommitment{ + AssetAmount: out1.AssetAmount, + VMVersion: out1.VMVersion, + ControlProgram: out1.ControlProgram, + SourceID: outres.SourceID, + SourcePosition: outres.SourcePos, + RefDataHash: outres.RefDataHash, + }, }, }, }, diff --git a/protocol/vm/crypto_test.go b/protocol/vm/crypto_test.go index 2220a6029a..c995b0b22f 100644 --- a/protocol/vm/crypto_test.go +++ b/protocol/vm/crypto_test.go @@ -88,7 +88,7 @@ func TestCheckSig(t *testing.T) { func TestCryptoOps(t *testing.T) { tx := bc.NewTx(bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, nil, bc.AssetID{}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(nil, bc.Hash{}, bc.AssetID{}, 5, 0, nil, bc.Hash{}, nil)}, Outputs: []*bc.TxOutput{}, }) @@ -409,10 +409,10 @@ func TestCryptoOps(t *testing.T) { tx: tx, txContext: txContext(&tx.TxData, 0), dataStack: [][]byte{{ - 64, 51, 183, 15, 140, 134, 122, 204, - 29, 162, 137, 33, 203, 185, 114, 69, - 247, 54, 170, 195, 70, 255, 127, 51, - 88, 80, 120, 226, 199, 219, 108, 22, + 47, 0, 60, 221, 100, 66, 123, 94, + 237, 214, 204, 181, 133, 71, 2, 11, + 2, 222, 242, 45, 197, 153, 126, 157, + 169, 172, 64, 73, 195, 74, 88, 216, }}, }, }, { diff --git a/protocol/vm/introspection_test.go b/protocol/vm/introspection_test.go index 7b46883a87..9c22043090 100644 --- a/protocol/vm/introspection_test.go +++ b/protocol/vm/introspection_test.go @@ -97,14 +97,17 @@ func TestBlockTime(t *testing.T) { func TestOutputIDAndNonceOp(t *testing.T) { var zeroHash bc.Hash - outputID := bc.Hash{3, 2, 1} nonce := []byte{36, 37, 38} tx := bc.NewTx(bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(outputID, nil, bc.AssetID{1}, 5, []byte("spendprog"), []byte("ref")), + bc.NewSpendInput(nil, bc.Hash{}, bc.AssetID{1}, 5, 0, []byte("spendprog"), bc.Hash{}, []byte("ref")), bc.NewIssuanceInput(nonce, 6, nil, zeroHash, []byte("issueprog"), nil, nil), }, }) + outputID, err := tx.Inputs[0].SpentOutputID() + if err != nil { + t.Fatal(err) + } vm := &virtualMachine{ runLimit: 50000, tx: tx, @@ -112,7 +115,7 @@ func TestOutputIDAndNonceOp(t *testing.T) { inputIndex: 0, program: []byte{uint8(OP_OUTPUTID)}, } - err := vm.step() + err = vm.step() if err != nil { t.Fatal(err) } @@ -163,7 +166,7 @@ func TestIntrospectionOps(t *testing.T) { tx := bc.NewTx(bc.TxData{ ReferenceData: []byte("txref"), Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, nil, bc.AssetID{1}, 5, []byte("spendprog"), []byte("ref")), + bc.NewSpendInput(nil, bc.Hash{}, bc.AssetID{1}, 5, 1, []byte("spendprog"), bc.Hash{}, []byte("ref")), bc.NewIssuanceInput(nil, 6, nil, bc.Hash{}, []byte("issueprog"), nil, nil), }, Outputs: []*bc.TxOutput{ diff --git a/protocol/vm/vm_test.go b/protocol/vm/vm_test.go index d1b36f4284..883ace2cc7 100644 --- a/protocol/vm/vm_test.go +++ b/protocol/vm/vm_test.go @@ -177,11 +177,13 @@ func TestVerifyTxInput(t *testing.T) { wantErr error }{{ input: bc.NewSpendInput( - bc.Hash{}, [][]byte{{2}, {3}}, + bc.Hash{}, bc.AssetID{}, 1, + 0, []byte{byte(OP_ADD), byte(OP_5), byte(OP_NUMEQUAL)}, + bc.Hash{}, nil, ), }, { @@ -207,7 +209,7 @@ func TestVerifyTxInput(t *testing.T) { }, { input: &bc.TxInput{ TypedInput: &bc.SpendInput{ - OutputCommitment: bc.OutputCommitment{ + SpendCommitment: bc.SpendCommitment{ VMVersion: 2, }, }, @@ -466,7 +468,7 @@ func TestVerifyTxInputQuickCheck(t *testing.T) { } }() tx := bc.NewTx(bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, witnesses, bc.AssetID{}, 10, program, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(witnesses, bc.Hash{}, bc.AssetID{}, 10, 0, program, bc.Hash{}, nil)}, }) verifyTxInput(tx, 0) return true From 077fadb74ec96e1712224032bcdbf4ea8c8c4575 Mon Sep 17 00:00:00 2001 From: Eric Rykwalder Date: Thu, 2 Mar 2017 10:31:11 -0800 Subject: [PATCH 2/4] fix accidental delete --- core/query/annotated.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/query/annotated.go b/core/query/annotated.go index 513d6cb62c..bfadf3e10b 100644 --- a/core/query/annotated.go +++ b/core/query/annotated.go @@ -160,6 +160,7 @@ func buildAnnotatedInput(tx *bc.Tx, i uint32) *AnnotatedInput { in.IssuanceProgram = prog } else { prevoutID := tx.SpentOutputIDs[i] + in.Type = "spend" in.ControlProgram = orig.ControlProgram() in.SpentOutputID = &prevoutID } From f190d29565114e0950640b178d716db5269f5601 Mon Sep 17 00:00:00 2001 From: Eric Rykwalder Date: Thu, 2 Mar 2017 10:55:17 -0800 Subject: [PATCH 3/4] doc it up --- protocol/bc/txhashes.go | 6 +++++- protocol/tx/map.go | 6 +++--- protocol/tx/transaction.go | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/protocol/bc/txhashes.go b/protocol/bc/txhashes.go index 614fe29032..5ff41c6c1a 100644 --- a/protocol/bc/txhashes.go +++ b/protocol/bc/txhashes.go @@ -11,7 +11,7 @@ type ( ID Hash ExpirationMS uint64 } - SpentOutputIDs []Hash + SpentOutputIDs []Hash // one per old-style Input. Non-spend inputs are blank hashes. VMContexts []*VMContext // one per old-style Input } @@ -52,4 +52,8 @@ func (t TxHashes) SigHash(n uint32) Hash { // to avoid a circular dependency between the bc and tx packages. var BlockHeaderHashFunc func(*BlockHeader) Hash +// OutputHash is initialized to a function in protocol/tx +// that can compute the hash of an output from a SpendCommitment. +// It is a variable here to avoid a circular dependency between +// the bc and tx packages. var OutputHash func(*SpendCommitment) (Hash, error) diff --git a/protocol/tx/map.go b/protocol/tx/map.go index c2e1682ce5..4a18777b90 100644 --- a/protocol/tx/map.go +++ b/protocol/tx/map.go @@ -33,13 +33,13 @@ func mapTx(tx *bc.TxData) (headerID bc.Hash, hdr *header, entryMap map[bc.Hash]e for i, inp := range tx.Inputs { if oldSp, ok := inp.TypedInput.(*bc.SpendInput); ok { - var oldSpID bc.Hash - oldSpID, err = ComputeOutputID(&oldSp.SpendCommitment) + var spendOutputID bc.Hash + spendOutputID, err = ComputeOutputID(&oldSp.SpendCommitment) if err != nil { return } var spID bc.Hash - spID, _, err = addEntry(newSpend(oldSpID, hashData(inp.ReferenceData), i)) + spID, _, err = addEntry(newSpend(spendOutputID, hashData(inp.ReferenceData), i)) if err != nil { err = errors.Wrapf(err, "adding spend entry for input %d", i) return diff --git a/protocol/tx/transaction.go b/protocol/tx/transaction.go index c536cbb82c..0806cd38c6 100644 --- a/protocol/tx/transaction.go +++ b/protocol/tx/transaction.go @@ -17,6 +17,8 @@ func init() { bc.OutputHash = ComputeOutputID } +// ComputeOutputID assembles an output entry given a spend commitment +// and computes and returns its corresponding entry ID. func ComputeOutputID(sc *bc.SpendCommitment) (h bc.Hash, err error) { defer func() { if r, ok := recover().(error); ok { From a710468360dfd4c7a1756909d8df890d41aa5cbc Mon Sep 17 00:00:00 2001 From: Eric Rykwalder Date: Thu, 2 Mar 2017 11:12:58 -0800 Subject: [PATCH 4/4] s/d/t throughout the codebase. jk --- protocol/tx/map.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/protocol/tx/map.go b/protocol/tx/map.go index 4a18777b90..5a426078c8 100644 --- a/protocol/tx/map.go +++ b/protocol/tx/map.go @@ -33,13 +33,13 @@ func mapTx(tx *bc.TxData) (headerID bc.Hash, hdr *header, entryMap map[bc.Hash]e for i, inp := range tx.Inputs { if oldSp, ok := inp.TypedInput.(*bc.SpendInput); ok { - var spendOutputID bc.Hash - spendOutputID, err = ComputeOutputID(&oldSp.SpendCommitment) + var spentOutputID bc.Hash + spentOutputID, err = ComputeOutputID(&oldSp.SpendCommitment) if err != nil { return } var spID bc.Hash - spID, _, err = addEntry(newSpend(spendOutputID, hashData(inp.ReferenceData), i)) + spID, _, err = addEntry(newSpend(spentOutputID, hashData(inp.ReferenceData), i)) if err != nil { err = errors.Wrapf(err, "adding spend entry for input %d", i) return