Skip to content

Commit

Permalink
slidechain: add double import protection (#52)
Browse files Browse the repository at this point in the history
Adds protection against double import.

Adds RPCs to `slidechaind` to wait on submitted TxVM transactions to hit the chain, and record peg-ins. Uses RPCs to create and submit the uniqueness token to TxVM, and modifies the import transaction to consume said token before issuing the pegged-in asset. Re-keys the `pegs` table on a unique transaction hash, and changes the schema of `pegs` also to track whether a transaction has hit the Stellar network. Moves insertion from the `watch` goroutine to between submitting the uniqueness token and the Stellar peg-in transaction. Updates associated tests.

https://trello.com/c/SnZp7YD3, 4-9
  • Loading branch information
debnil committed Feb 5, 2019
1 parent d5e42ac commit b86c15f
Show file tree
Hide file tree
Showing 13 changed files with 416 additions and 128 deletions.
132 changes: 120 additions & 12 deletions slidechain/cmd/peg/peg.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
package main

import (
"bytes"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"

"github.com/chain/txvm/crypto/ed25519"
"github.com/interstellar/slingshot/slidechain/stellar"
"github.com/chain/txvm/errors"
"github.com/chain/txvm/protocol/bc"
"github.com/golang/protobuf/proto"
"github.com/stellar/go/clients/horizon"
"github.com/stellar/go/xdr"

"github.com/interstellar/slingshot/slidechain"
"github.com/interstellar/slingshot/slidechain/stellar"
)

func main() {
var (
custodian = flag.String("custodian", "", "Stellar account ID of custodian account")
amount = flag.String("amount", "", "amount to peg, in lumens")
recipient = flag.String("recipient", "", "hex-encoded txvm public key for the recipient of the pegged funds")
seed = flag.String("seed", "", "seed of Stellar source account")
horizonURL = flag.String("horizon", "https://horizon-testnet.stellar.org", "horizon URL")
code = flag.String("code", "", "asset code for non-Lumen asset")
issuer = flag.String("issuer", "", "asset issuer for non-Lumen asset")
custodian = flag.String("custodian", "", "Stellar account ID of custodian account")
amount = flag.String("amount", "", "amount to peg, in lumens")
recipient = flag.String("recipient", "", "hex-encoded txvm public key for the recipient of the pegged funds")
seed = flag.String("seed", "", "seed of Stellar source account")
horizonURL = flag.String("horizon", "https://horizon-testnet.stellar.org", "horizon URL")
code = flag.String("code", "", "asset code for non-Lumen asset")
issuer = flag.String("issuer", "", "asset issuer for non-Lumen asset")
bcidHex = flag.String("bcid", "", "hex-encoded initial block ID")
slidechaind = flag.String("slidechaind", "http://127.0.0.1:2423", "url of slidechaind server")
)
flag.Parse()

Expand All @@ -34,6 +46,9 @@ func main() {
if (*code == "" && *issuer != "") || (*code != "" && *issuer == "") {
log.Fatal("must specify both code and issuer for non-Lumen asset")
}
if *bcidHex == "" {
log.Fatal("must specify initial block ID")
}
if *recipient == "" {
log.Print("no recipient specified, generating txvm keypair...")
pubkey, privkey, err := ed25519.GenerateKey(nil)
Expand All @@ -59,20 +74,113 @@ func main() {
}
_, err := hex.Decode(recipientPubkey[:], []byte(*recipient))
if err != nil {
log.Fatal(err, "decoding recipient")
log.Fatal("decoding recipient: ", err)
}
var bcidBytes [32]byte
_, err = hex.Decode(bcidBytes[:], []byte(*bcidHex))
if err != nil {
log.Fatal("decoding initial block id: ", err)
}
var asset xdr.Asset
if *issuer != "" {
var issuerID xdr.AccountId
err = issuerID.SetAddress(*issuer)
if err != nil {
log.Fatal("setting issuer ID: ", err)
}
err = asset.SetCredit(*code, issuerID)
if err != nil {
log.Fatal("setting asset code and issuer: ", err)
}
} else {
asset, err = xdr.NewAsset(xdr.AssetTypeAssetTypeNative, nil)
if err != nil {
log.Fatal("setting native asset: ", err)
}
}

assetXDR, err := asset.MarshalBinary()
if err != nil {
log.Fatal("marshaling asset xdr: ", err)
}
amountInt, err := strconv.ParseInt(*amount, 10, 64)
if err != nil {
log.Fatal("converting amount to int64: ", err)
}
expMS := int64(bc.Millis(time.Now().Add(10 * time.Minute)))
err = doPrepegTx(bcidBytes[:], assetXDR, amountInt, expMS, recipientPubkey[:], *slidechaind)
if err != nil {
log.Fatal("doing pre-peg-in tx: ", err)
}
hclient := &horizon.Client{
URL: strings.TrimRight(*horizonURL, "/"),
HTTP: new(http.Client),
}
tx, err := stellar.BuildPegInTx(*seed, recipientPubkey, *amount, *code, *issuer, *custodian, hclient)
nonceHash := slidechain.UniqueNonceHash(bcidBytes[:], expMS)
tx, err := stellar.BuildPegInTx(*seed, nonceHash, *amount, *code, *issuer, *custodian, hclient)
if err != nil {
log.Fatal(err, "building transaction")
log.Fatal("building transaction: ", err)
}
succ, err := stellar.SignAndSubmitTx(hclient, tx, *seed)
if err != nil {
log.Fatal(err, "submitting peg-in tx")
log.Fatal("submitting peg-in tx: ", err)
}
log.Printf("successfully submitted peg-in tx hash %s on ledger %d", succ.Hash, succ.Ledger)
}

// DoPrepegTx builds, submits the pre-peg TxVM transaction, and waits for it to hit the chain.
func doPrepegTx(bcid, assetXDR []byte, amount, expMS int64, pubkey ed25519.PublicKey, slidechaind string) error {
prepegTx, err := slidechain.BuildPrepegTx(bcid, assetXDR, pubkey, amount, expMS)
if err != nil {
return errors.Wrap(err, "building pre-peg-in tx")
}
err = submitPrepegTx(prepegTx, slidechaind)
if err != nil {
return errors.Wrap(err, "submitting and waiting on pre-peg-in tx")
}
err = recordPeg(prepegTx.ID, assetXDR, amount, expMS, pubkey, slidechaind)
if err != nil {
return errors.Wrap(err, "recording peg")
}
return nil
}

func submitPrepegTx(tx *bc.Tx, slidechaind string) error {
prepegTxBits, err := proto.Marshal(&tx.RawTx)
if err != nil {
return errors.Wrap(err, "marshaling pre-peg tx")
}
resp, err := http.Post(slidechaind+"/submit?wait=1", "application/octet-stream", bytes.NewReader(prepegTxBits))
if err != nil {
return errors.Wrap(err, "submitting to slidechaind")
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("status code %d from POST /submit", resp.StatusCode)
}
log.Printf("successfully submitted and waited on pre-peg-in tx %x", tx.ID)
return nil
}

func recordPeg(txid bc.Hash, assetXDR []byte, amount, expMS int64, pubkey ed25519.PublicKey, slidechaind string) error {
p := slidechain.Peg{
Amount: amount,
AssetXDR: assetXDR,
RecipPubkey: pubkey,
ExpMS: expMS,
}
pegBits, err := json.Marshal(&p)
if err != nil {
return errors.Wrap(err, "marshaling peg")
}
resp, err := http.Post(slidechaind+"/record", "application/octet-stream", bytes.NewReader(pegBits))
if err != nil {
return errors.Wrap(err, "recording to slidechaind")
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("status code %d from POST /record", resp.StatusCode)
}
log.Printf("successfully recorded peg for tx %x", txid.Bytes())
return nil
}
1 change: 1 addition & 0 deletions slidechain/cmd/slidechaind/slidechaind.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ func main() {
http.Handle("/submit", c.S)
http.HandleFunc("/get", c.S.Get)
http.HandleFunc("/account", c.Account)
http.HandleFunc("/record", c.RecordPeg)
http.Serve(listener, nil)
}
4 changes: 2 additions & 2 deletions slidechain/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func BuildExportTx(ctx context.Context, asset xdr.Asset, amount, inputAmt int64,
if err != nil {
return nil, err
}
assetID := bc.NewHash(txvm.AssetID(issueSeed[:], assetBytes))
assetID := bc.NewHash(txvm.AssetID(importIssuanceSeed[:], assetBytes))
var rawSeed [32]byte
copy(rawSeed[:], prv)
kp, err := keypair.FromRawSeed(rawSeed)
Expand Down Expand Up @@ -359,7 +359,7 @@ func IsExportTx(tx *bc.Tx, asset xdr.Asset, inputAmt int64, temp, exporter strin
if err != nil {
return false
}
wantAssetID := txvm.AssetID(issueSeed[:], assetBytes)
wantAssetID := txvm.AssetID(importIssuanceSeed[:], assetBytes)
if !bytes.Equal(wantAssetID[:], tx.Log[2][3].(txvm.Bytes)) {
return false
}
Expand Down
70 changes: 30 additions & 40 deletions slidechain/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"log"
"math"
"time"

"github.com/bobg/sqlutil"
"github.com/chain/txvm/crypto/ed25519"
Expand All @@ -20,34 +19,28 @@ import (

// buildImportTx builds the import transaction.
func (c *Custodian) buildImportTx(
amount int64,
assetXDR []byte,
recipPubkey []byte,
amount, expMS int64,
assetXDR, recipPubkey []byte,
) ([]byte, error) {
// Push asset code, amount, exp, blockid onto the arg stack.
// Input plain-data consume token contract and put it on the arg stack.
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "x'%x' put\n", assetXDR)
fmt.Fprintf(buf, "%d put\n", amount)
exp := int64(bc.Millis(time.Now().Add(5 * time.Minute)))

// now arg stack is set up, empty con stack
fmt.Fprintf(buf, "x'%x' %d\n", c.InitBlockHash.Bytes(), exp) // con stack: blockid, exp
fmt.Fprintf(buf, "nonce put\n") // empty con stack; ..., nonce on arg stack
fmt.Fprintf(buf, "x'%x' contract call\n", issueProg) // empty con stack; arg stack: ..., sigcheck, issuedval

// pay issued value
fmt.Fprintf(buf, "get splitzero\n") // con stack: issuedval, zeroval; arg stack: sigcheck
fmt.Fprintf(buf, "'' put\n") // con stack: issuedval, zeroval; arg stack: sigcheck, refdata
fmt.Fprintf(buf, "swap put\n") // con stack: zeroval; arg stack: sigcheck, refdata, issuedval
fmt.Fprintf(buf, "{x'%x'} put\n", recipPubkey) // con stack: zeroval; arg stack: sigcheck, refdata, issuedval, {recippubkey}
fmt.Fprintf(buf, "1 put\n") // con stack: zeroval; arg stack: sigcheck, refdata, issuedval, {recippubkey}, quorum
fmt.Fprintf(buf, "x'%x' contract call\n", standard.PayToMultisigProg1) // con stack: zeroval; arg stack: sigcheck
fmt.Fprintf(buf, "{'C', x'%x', x'%x',", createTokenSeed[:], consumeTokenProg)
fmt.Fprintf(buf, " {'Z', %d}, {'T', {x'%x'}},", int64(1), recipPubkey)
nonceHash := UniqueNonceHash(c.InitBlockHash.Bytes(), expMS)
fmt.Fprintf(buf, " {'V', %d, x'%x', x'%x'},", 0, zeroSeed[:], nonceHash[:])
fmt.Fprintf(buf, " {'Z', %d}, {'S', x'%x'}}", amount, assetXDR)
fmt.Fprintf(buf, " input put\n") // arg stack: consumeTokenContract
fmt.Fprintf(buf, "x'%x' contract call\n", importIssuanceProg) // arg stack: sigchecker, issuedval, {recip}, quorum
fmt.Fprintf(buf, "get get get splitzero\n") // con stack: quorum, {recip}, issuedval, zeroval; arg stack: sigchecker
fmt.Fprintf(buf, "3 bury\n") // con stack: zeroval, quorum, {recip}, issuedval; arg stack: sigchecker
fmt.Fprintf(buf, "'' put\n") // con stack: zeroval, quorum, {recip}, issuedval; arg stack: sigchecker, refdata
fmt.Fprintf(buf, "put put put\n") // con stack: zeroval; arg stack: sigchecker, refdata, issuedval, {recip}, quorum
fmt.Fprintf(buf, "x'%x' contract call\n", standard.PayToMultisigProg1) // con stack: zeroval; arg stack: sigchecker
fmt.Fprintf(buf, "finalize\n")
tx1, err := asm.Assemble(buf.String())
if err != nil {
return nil, errors.Wrap(err, "assembling payment tx")
}

vm, err := txvm.Validate(tx1, 3, math.MaxInt64, txvm.StopAfterFinalize)
if err != nil {
return nil, errors.Wrap(err, "computing transaction ID")
Expand Down Expand Up @@ -85,33 +78,31 @@ func (c *Custodian) importFromPegs(ctx context.Context) {
}

var (
txids []string
opNums []int
amounts []int64
assetXDRs, recips [][]byte
amounts, expMSs []int64
nonceHashes, assetXDRs, recips [][]byte
)
const q = `SELECT txid, operation_num, amount, asset_xdr, recipient_pubkey FROM pegs WHERE imported=0`
err := sqlutil.ForQueryRows(ctx, c.DB, q, func(txid string, opNum int, amount int64, assetXDR, recip []byte) {
txids = append(txids, txid)
opNums = append(opNums, opNum)
const q = `SELECT nonce_hash, amount, asset_xdr, recipient_pubkey, nonce_expms FROM pegs WHERE imported=0 AND stellar_tx=1`
err := sqlutil.ForQueryRows(ctx, c.DB, q, func(nonceHash []byte, amount int64, assetXDR, recip []byte, expMS int64) {
nonceHashes = append(nonceHashes, nonceHash)
amounts = append(amounts, amount)
assetXDRs = append(assetXDRs, assetXDR)
recips = append(recips, recip)
expMSs = append(expMSs, expMS)
})
if err == context.Canceled {
return
}
if err != nil {
log.Fatalf("querying pegs: %s", err)
}
for i, txid := range txids {
for i, nonceHash := range nonceHashes {
var (
opNum = opNums[i]
amount = amounts[i]
assetXDR = assetXDRs[i]
recip = recips[i]
expMS = expMSs[i]
)
err = c.doImport(ctx, txid, opNum, amount, assetXDR, recip)
err = c.doImport(ctx, nonceHash, amount, assetXDR, recip, expMS)
if err != nil {
if err == context.Canceled {
return
Expand All @@ -122,10 +113,9 @@ func (c *Custodian) importFromPegs(ctx context.Context) {
}
}

func (c *Custodian) doImport(ctx context.Context, txid string, opNum int, amount int64, assetXDR, recip []byte) error {
log.Printf("doing import from tx %s, op %d: %d of asset %x for recipient %x", txid, opNum, amount, assetXDR, recip)

importTxBytes, err := c.buildImportTx(amount, assetXDR, recip)
func (c *Custodian) doImport(ctx context.Context, nonceHash []byte, amount int64, assetXDR, recip []byte, expMS int64) error {
log.Printf("doing import from tx with hash %x: %d of asset %x for recipient %x with expiration %d", nonceHash, amount, assetXDR, recip, expMS)
importTxBytes, err := c.buildImportTx(amount, expMS, assetXDR, recip)
if err != nil {
return errors.Wrap(err, "building import tx")
}
Expand All @@ -135,12 +125,12 @@ func (c *Custodian) doImport(ctx context.Context, txid string, opNum int, amount
return errors.Wrap(err, "computing transaction ID")
}
importTx.Runlimit = math.MaxInt64 - runlimit
err = c.S.submitTx(ctx, importTx)
_, err = c.S.submitTx(ctx, importTx)
if err != nil {
return errors.Wrap(err, "submitting import tx")
}
txresult := txresult.New(importTx)
log.Printf("assetID %x amount %d anchor %x\n", txresult.Issuances[0].Value.AssetID.Bytes(), txresult.Issuances[0].Value.Amount, txresult.Issuances[0].Value.Anchor)
_, err = c.DB.ExecContext(ctx, `UPDATE pegs SET imported=1 WHERE txid = $1 AND operation_num = $2`, txid, opNum)
return errors.Wrapf(err, "setting imported=1 for txid %s, operation %d", txid, opNum)
_, err = c.DB.ExecContext(ctx, `UPDATE pegs SET imported=1 WHERE nonce_hash = $1`, nonceHash)
return errors.Wrapf(err, "setting imported=1 for tx with hash %x", nonceHash)
}
13 changes: 1 addition & 12 deletions slidechain/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package slidechain

import (
"fmt"
"log"

"github.com/chain/txvm/protocol/txvm"
"github.com/chain/txvm/protocol/txvm/asm"
)

Expand All @@ -20,14 +18,5 @@ const issueProgFmt = `

var (
issueProgSrc = fmt.Sprintf(issueProgFmt, custodianPub)
issueProg = mustAssemble(issueProgSrc)
issueSeed = txvm.ContractSeed(issueProg)
issueProg = asm.MustAssemble(issueProgSrc)
)

func mustAssemble(inp string) []byte {
result, err := asm.Assemble(inp)
if err != nil {
log.Fatal(err)
}
return result
}

0 comments on commit b86c15f

Please sign in to comment.