bitcoin-payment-channel 0.5.0.1 → 0.6.0.0
raw patch · 18 files changed
+477/−296 lines, 18 filesdep ~QuickCheck
Dependency ranges changed: QuickCheck
Files
- bitcoin-payment-channel.cabal +16/−10
- src/Data/Bitcoin/PaymentChannel.hs +57/−37
- src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Amount.hs +24/−17
- src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Fee.hs +28/−0
- src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Script.hs +7/−3
- src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Util.hs +1/−1
- src/Data/Bitcoin/PaymentChannel/Internal/Crypto/PubKey.hs +30/−0
- src/Data/Bitcoin/PaymentChannel/Internal/Error.hs +9/−13
- src/Data/Bitcoin/PaymentChannel/Internal/Refund.hs +21/−10
- src/Data/Bitcoin/PaymentChannel/Internal/Serialization.hs +12/−11
- src/Data/Bitcoin/PaymentChannel/Internal/Settlement.hs +40/−28
- src/Data/Bitcoin/PaymentChannel/Internal/State.hs +13/−1
- src/Data/Bitcoin/PaymentChannel/Internal/Types.hs +40/−29
- src/Data/Bitcoin/PaymentChannel/Internal/Util.hs +4/−0
- src/Data/Bitcoin/PaymentChannel/Test.hs +113/−0
- src/Data/Bitcoin/PaymentChannel/Types.hs +42/−22
- src/Data/Bitcoin/PaymentChannel/Util.hs +4/−4
- test/Main.hs +16/−110
bitcoin-payment-channel.cabal view
@@ -1,5 +1,5 @@ name: bitcoin-payment-channel-version: 0.5.0.1+version: 0.6.0.0 synopsis: Library for working with Bitcoin payment channels description: A Bitcoin payment channel allows secure and instant transfer of bitcoins from one@@ -34,22 +34,25 @@ exposed-modules: Data.Bitcoin.PaymentChannel Data.Bitcoin.PaymentChannel.Types Data.Bitcoin.PaymentChannel.Util- -- Exposed for testing purposes- Data.Bitcoin.PaymentChannel.Internal.State+ Data.Bitcoin.PaymentChannel.Test - other-modules: Data.Bitcoin.PaymentChannel.Internal.Error+ other-modules: Data.Bitcoin.PaymentChannel.Internal.State+ Data.Bitcoin.PaymentChannel.Internal.Error Data.Bitcoin.PaymentChannel.Internal.Payment Data.Bitcoin.PaymentChannel.Internal.Refund Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Util Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime+ Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Fee+ Data.Bitcoin.PaymentChannel.Internal.Crypto.PubKey Data.Bitcoin.PaymentChannel.Internal.Settlement Data.Bitcoin.PaymentChannel.Internal.Types Data.Bitcoin.PaymentChannel.Internal.Serialization Data.Bitcoin.PaymentChannel.Internal.Util - ghc-options: -W++ ghc-options: -Wall default-language: Haskell2010 build-depends: base >= 4.7 && < 5,@@ -64,7 +67,8 @@ aeson >= 0.11.0 && < 0.12.0, scientific >= 0.3.0 && < 0.4.0, string-conversions >= 0.4 && < 0.5,- tagged >= 0.8 && < 1.0+ tagged >= 0.8 && < 1.0,+ QuickCheck >= 2.8 && < 2.9 hs-source-dirs: src @@ -74,10 +78,12 @@ main-is: Main.hs default-language: Haskell2010 hs-source-dirs: test- ghc-options: -W+ ghc-options: -Wall - build-depends: base >= 4.7 && < 5,- haskoin-core >= 0.4.0 && < 1.0.0,+ build-depends: base >= 4.7 && < 5,+ haskoin-core >= 0.4.0 && < 1.0.0,+ QuickCheck >= 2.8 && < 2.9,+ bytestring, base16-bytestring, base64-bytestring, hexstring, text,@@ -85,5 +91,5 @@ time, cereal, aeson, - QuickCheck, test-framework, test-framework-quickcheck2,+ test-framework, test-framework-quickcheck2, bitcoin-payment-channel
src/Data/Bitcoin/PaymentChannel.hs view
@@ -8,12 +8,11 @@ Portability : POSIX In order to set up a payment channel between a sender and a receiver, the two parties must- first agree on four parameters for the channel:+ first agree on three [1] parameters for the channel: (1) sender public key (2) receiver public key (3) channel expiration date- (4) sender change output "dust limit" (see note [1]) These parameters are contained in 'ChannelParameters', from which the Bitcoin address used to fund@@ -68,11 +67,21 @@ signing function passed to 'getSettlementBitcoinTx', where @receiverPrivKey@ is the private key from which 'cpReceiverPubKey' is derived. -[1] The minimum amount that the server is willing to accept as the client change value in the- payment transaction. Set this to 700 and you'll be fine. It's only relevant if the channel is+[1] In addition to this, two configuration options must also be agreed upon: a "dust limit",+ and a "settlement period" (measured in hours), which is subtracted from the channel expiration+ date in order to arrive at the effective (earlier) expiration date. This is necessary to give the+ server/receiver time to publish the settlement transaction before the refund transaction becomes+ valid. The dust limit is the minimum amount that the server is willing to accept+ as the client change value in the+ payment transaction. It's only relevant if the channel is emptied of value completely, but it is necessary because the server doesn't want to accept payments based on transactions it cannot publish via the Bitcoin P2P network, because they- contain an output of minuscule value.+ contain an output of minuscule value. Sensible values are contained in 'defaultConfig', but+ client/sender and server/receiver need to agree on these parameter as well, although they are+ only relevant 1) (in the case of the dust limit) if the channel is compleltely exchausted and+ 2) (in case of the "settlemend period") if the client tries to make payments close to expiration+ (and, in case the client does, it will just receive an error in response, saying the channel+ is now closed). __/IMPORTANT:/__ /Channel setup is risk free because the sender can derive a refund Bitcoin transaction/ /using 'getRefundBitcoinTx', which returns the bitcoins used to fund the channel back to the sender./@@ -88,22 +97,29 @@ module Data.Bitcoin.PaymentChannel (- channelWithInitialPaymentOf,- sendPayment,+ -- *Initialization + -- **Funding+ getFundingAddress,++ -- **State creation+ channelWithInitialPaymentOf, channelFromInitialPayment,++ -- *Payment+ sendPayment, recvPayment,- recvPaymentForClose, + -- *Settlement+ recvPaymentForClose, getSettlementBitcoinTx,- getRefundBitcoinTx,-- getFundingAddress+ getRefundBitcoinTx ) where import Data.Bitcoin.PaymentChannel.Internal.Types (PaymentTxConfig(..), Payment(..), FullPayment(..), Config,+ ReceiverPaymentChannelI(..), PaymentChannelState, pcsConfig, pcsClientChangeVal, pcsParameters) import Data.Bitcoin.PaymentChannel.Internal.State (newPaymentChannelState, updatePaymentChannelState, isPastLockTimeDate)@@ -112,8 +128,7 @@ (createPayment, paymentFromState, verifyPaymentSigFromState) import Data.Bitcoin.PaymentChannel.Internal.Settlement (signedSettlementTxFromState)-import Data.Bitcoin.PaymentChannel.Internal.Refund- (refundTxAddSignature, getRefundTxHashForSigning)+import Data.Bitcoin.PaymentChannel.Internal.Refund (mkRefundTx) import Data.Bitcoin.PaymentChannel.Util (getFundingAddress) import Data.Bitcoin.PaymentChannel.Types@@ -130,9 +145,9 @@ -- Returns a new 'SenderPaymentChannel' state object and the first channel payment. channelWithInitialPaymentOf :: Config -- ^ Various configuration options. 'defaultConfig' contains sensible defaults.- -> ChannelParameters -- ^ Specifies channel sender and receiver, channel expiration date and "dust limit"+ -> ChannelParameters -- ^ Specifies channel sender and receiver, channel expiration date -> FundingTxInfo -- ^ Holds information about the Bitcoin transaction used to fund the channel- -> (HC.Hash256 -> HC.Signature) -- ^ Used to sign payments from sender. When given a 'HC.Hash256', produces a signature that verifies against sender PubKey.+ -> (HC.Hash256 -> HC.Signature) -- ^ Used to sign payments from sender. When given a 'HC.Hash256', produces a signature that verifies against sender pubkey. -> HC.Address -- ^ Value sender/client change address -> BitcoinAmount -- ^ Value of initial payment. Can be zero. -> (BitcoinAmount, FullPayment, SenderPaymentChannel) -- ^Initial payment amount (may be capped), initial payment, and new sender state object@@ -167,12 +182,13 @@ -- Will not be accepted by the Bitcoin network until the expiration time specified in -- 'ChannelParameters'. Receiver beware of Bitcoin network time drift and the -- unpreditable nature of finding new blocks.-getRefundBitcoinTx ::- SenderPaymentChannel -- ^Sender state object- -> BitcoinAmount -- ^Refund transaction fee- -> HT.Tx -- ^Refund Bitcoin transaction+getRefundBitcoinTx+ :: HasFee fee+ => SenderPaymentChannel -- ^Sender state object+ -> fee -- ^Refund transaction fee+ -> HT.Tx -- ^Refund Bitcoin transaction getRefundBitcoinTx (CSenderPaymentChannel cs signFunc) txFee =- refundTxAddSignature cs txFee $ signFunc $ getRefundTxHashForSigning cs txFee+ mkRefundTx cs txFee signFunc -- |Create new 'ReceiverPaymentChannel'.@@ -192,17 +208,17 @@ in flip (recvPayment now) fp $ CReceiverPaymentChannel -- Create a new state with the unverified signature; then verify the same signature in 'recvPayment'- (newPaymentChannelState cfg cp fundInf pConf sig)+ (newPaymentChannelState cfg cp fundInf pConf sig) () -- |Register, on the receiving side, a payment made by 'sendPayment' on the sending side. -- Returns error if either the signature or payment amount is invalid, and otherwise -- the amount received with this 'Payment' and a new state object. recvPayment ::- UTCTime -- ^Current time. Needed for payment verification.- -> ReceiverPaymentChannel -- ^Receiver state object- -> FullPayment -- ^Payment to verify and register- -> Either PayChanError (BitcoinAmount, ReceiverPaymentChannel) -- ^Value received plus new receiver state object-recvPayment currentTime rpc@(CReceiverPaymentChannel oldState) fp@(CFullPayment paymnt _ _ _) =+ UTCTime -- ^Current time. Needed for payment verification.+ -> ReceiverPaymentChannelI a -- ^Receiver state object+ -> FullPayment -- ^Payment to verify and register+ -> Either PayChanError (BitcoinAmount, ReceiverPaymentChannelI a) -- ^Value received plus new receiver state object+recvPayment currentTime rpc@(CReceiverPaymentChannel oldState _) fp@(CFullPayment paymnt _ _ _) = updatePaymentChannelState oldState fp >>= checkExpiration >>= verifyPaymentSignature paymnt >>=@@ -231,16 +247,19 @@ -- with a new client change address. Used to produce the settlement -- transaction that returns unsent funds to the client. recvPaymentForClose ::- ReceiverPaymentChannel -- ^Receiver state object- -> FullPayment -- ^Payment to verify and register- -> Either PayChanError ReceiverPaymentChannel -- ^ Receiver state object-recvPaymentForClose (CReceiverPaymentChannel state) fp =+ ReceiverPaymentChannelI a -- ^Receiver state object+ -> FullPayment -- ^Payment to verify and register+ -> Either PayChanError (ReceiverPaymentChannelI a) -- ^ Receiver state object+recvPaymentForClose (CReceiverPaymentChannel state pki) fp = recvPayment futureTimeStamp newAddressState fp >>= \(amtRecv, newState) -> case amtRecv of 0 -> Right newState _ -> Left ClosingPaymentBadValue- where newAddressState = CReceiverPaymentChannel $- S.setClientChangeAddress state (fpChangeAddr fp)+ where newAddressState = CReceiverPaymentChannel+ { rpcState = S.setClientChangeAddress state (fpChangeAddr fp)+ , rpcPubKeyInfo = pki+ }+ -- When receiving a payment of zero value, which only modifies the -- client change address, we don't care about verifying that the channel -- hasn't expired yet (which it may well have, since the client wants its@@ -252,13 +271,14 @@ -- is included in a Bitcoin block before the refund transaction becomes valid (see 'getRefundBitcoinTx'). -- The sender can only close the channel before expiration by requesting this transaction -- from the receiver and publishing it to the Bitcoin network.-getSettlementBitcoinTx ::- ReceiverPaymentChannel -- ^ Receiver state object- -> (HC.Hash256 -> HC.Signature) -- ^ Function which produces a signature which verifies against 'cpReceiverPubKey'+getSettlementBitcoinTx+ :: HasFee fee+ => ReceiverPaymentChannelI a -- ^ Receiver state object -> HC.Address -- ^ Receiver destination address. Funds sent over the channel will be sent to this address, the rest back to the client change address (an argument to 'channelWithInitialPaymentOf').- -> BitcoinAmount -- ^ Bitcoin transaction fee+ -> (HC.Hash256 -> HC.Signature) -- ^ Function which produces a signature which verifies against 'cpReceiverPubKey'+ -> fee -- ^ Bitcoin transaction fee -> HT.Tx -- ^ Settling Bitcoin transaction-getSettlementBitcoinTx (CReceiverPaymentChannel cs) =+getSettlementBitcoinTx (CReceiverPaymentChannel cs _) = signedSettlementTxFromState cs
src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Amount.hs view
@@ -4,31 +4,31 @@ import qualified Data.Serialize.Put as SerPut import qualified Data.Serialize.Get as SerGet import Data.Word+import Data.Ratio -- |Represents a bitcoin amount as number of satoshis.--- 1 satoshi = 1e-8 bitcoin.--- Only natural numbers (>= 0) can be represented ('fromInteger' caps to a 'Word64').--- It is thus not possible to construct a negative BitcoinAmount which, when added to--- another BitcoinAmount, subtracts from its value. So we avoid the problem of being--- able to invert subtraction/addition by supplying a negative value as one of the--- arguments.+-- 1 satoshi = 1e-8 bitcoin. 1e8 satohis = 1 bitcoin.+-- Only amounts >= 0 can be represented, and 'fromInteger' caps to a 'Word64'.+-- It is thus not possible to eg. construct a negative BitcoinAmount which, when added to+-- another BitcoinAmount, subtracts from its value. Adding two large amounts together will+-- never overflow, nor will subtraction underflow. newtype BitcoinAmount = BitcoinAmount Integer deriving (Eq, Ord) instance Show BitcoinAmount where show amount = show (toInteger amount) ++ " satoshi" instance Num BitcoinAmount where- (BitcoinAmount a1) * (BitcoinAmount a2) = BitcoinAmount (fromIntegral . capToWord64 $ a1*a2)- (BitcoinAmount a1) + (BitcoinAmount a2) = BitcoinAmount (fromIntegral . capToWord64 $ a1+a2)- (BitcoinAmount a1) - (BitcoinAmount a2) = BitcoinAmount (fromIntegral . capToWord64 $ a1-a2)+ (BitcoinAmount a1) * (BitcoinAmount a2) = BitcoinAmount (fromIntegral . capTo21Mill $ a1*a2)+ (BitcoinAmount a1) + (BitcoinAmount a2) = BitcoinAmount (fromIntegral . capTo21Mill $ a1+a2)+ (BitcoinAmount a1) - (BitcoinAmount a2) = BitcoinAmount (fromIntegral . capTo21Mill $ a1-a2) abs = id -- Always positive signum (BitcoinAmount 0) = BitcoinAmount 0 signum (BitcoinAmount _) = BitcoinAmount 1- fromInteger = BitcoinAmount . fromIntegral . capToWord64+ fromInteger = BitcoinAmount . fromIntegral . capTo21Mill instance Enum BitcoinAmount where- toEnum = BitcoinAmount . fromIntegral . capToWord64 . fromIntegral+ toEnum = BitcoinAmount . fromIntegral . capTo21Mill . fromIntegral fromEnum (BitcoinAmount amount) = fromIntegral amount instance Real BitcoinAmount where@@ -36,15 +36,22 @@ instance Integral BitcoinAmount where toInteger (BitcoinAmount int) = int- quotRem (BitcoinAmount _) (BitcoinAmount _) =- error "Division of two BitcoinAmounts is undefined"+ quotRem (BitcoinAmount a1) (BitcoinAmount a2) =+ ( BitcoinAmount (fromIntegral . capTo21Mill $ res1)+ , BitcoinAmount (fromIntegral . capTo21Mill $ res2)+ )+ where (res1,res2) = quotRem a1 a2 --- | Convert to 'Word64', with zero as floor, (maxBound :: Word64) as ceiling-capToWord64 :: Integer -> Word64-capToWord64 i = fromIntegral $+instance Bounded BitcoinAmount where+ minBound = BitcoinAmount 0+ maxBound = BitcoinAmount $ round $ (21e6 :: Ratio Integer) * (1e8 :: Ratio Integer)++-- | Convert to 21 million, zero as floor+capTo21Mill :: Integer -> Word64+capTo21Mill i = fromIntegral $ max 0 cappedValue where- cappedValue = min i $ fromIntegral (maxBound :: Word64)+ cappedValue = min i $ fromIntegral (maxBound :: BitcoinAmount) instance Ser.Serialize BitcoinAmount where put = SerPut.putWord64le . fromIntegral . toInteger
+ src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Fee.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Fee where++import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount+import qualified Data.Serialize as Bin+++-- |Objects from which a Bitcoin fee can be calculated,+-- given a transaction (yeah you need to create a tx twice).+class HasFee a where+ absoluteFee :: TxByteSize -> a -> BitcoinAmount++-- |For compatibility+instance HasFee BitcoinAmount where+ absoluteFee _ = id -- Same as constant fee++data Constant = Constant BitcoinAmount+instance HasFee Constant where+ absoluteFee _ (Constant amt) = amt++type TxByteSize = Word+-- |Specify a fee as satoshis per byte+newtype SatoshisPerByte = SatoshisPerByte BitcoinAmount -- ^Fee in satoshis per byte+ deriving (Eq, Show, Bin.Serialize, Ord, Num, Enum, Real, Integral)+instance HasFee SatoshisPerByte where+ absoluteFee txByteSize (SatoshisPerByte satoshisPerByte) =+ fromIntegral txByteSize * satoshisPerByte+
src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Script.hs view
@@ -16,7 +16,10 @@ -- |Generates OP_CHECKLOCKTIMEVERIFY redeemScript, which can be redeemed in two ways: -- 1) by providing a signature from both server and client -- 2) after the date specified by lockTime: by providing only a client signature-paymentChannelRedeemScript :: SendPubKey -> RecvPubKey -> BitcoinLockTime -> Script+paymentChannelRedeemScript :: SendPubKey+ -> RecvPubKey+ -> BitcoinLockTime+ -> Script paymentChannelRedeemScript clientPK serverPK lockTime = let -- Note: HI.encodeInt encodes values up to and including 2^31-1 as 4 bytes@@ -33,7 +36,7 @@ [OP_IF, opPushData $ serialize serverPubKey, OP_CHECKSIGVERIFY, OP_ELSE,- encodeScriptInt $ lockTime, op_CHECKLOCKTIMEVERIFY, OP_DROP,+ encodeScriptInt lockTime, op_CHECKLOCKTIMEVERIFY, OP_DROP, OP_ENDIF, opPushData $ serialize clientPubKey, OP_CHECKSIG] @@ -51,7 +54,8 @@ refundTxScriptSig :: HC.Signature -> Script refundTxScriptSig clientSig = Script [opPushData (B.append (serialize clientSig) hashTypeByte),- OP_0] -- Signal that we want to provide only one PubKey+ OP_0] -- Signal that we want to provide only one pubkey/sig pair (sender's),+ -- after it is checked that the lockTime has expired in the script where hashTypeByte = serialize (SigAll False) -----Util-----
src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Util.hs view
@@ -52,7 +52,7 @@ HT.createTx (HT.txVersion tx) (HT.txIn tx) ( oldOuts ++ [txOut] ) (HT.txLockTime tx) where oldOuts = HT.txOut tx -bitcoinPayPK :: HC.PubKey -> HS.Script+bitcoinPayPK :: HC.PubKeyC -> HS.Script bitcoinPayPK pk = HS.encodeOutput $ HS.PayPKHash $ HC.pubKeyAddr pk bitcoinPayPKBS = serialize . bitcoinPayPK where serialize = Ser.encode
+ src/Data/Bitcoin/PaymentChannel/Internal/Crypto/PubKey.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Data.Bitcoin.PaymentChannel.Internal.Crypto.PubKey+( IsPubKey(..)+, SendPubKey(..)+, RecvPubKey(..)+) where++import qualified Network.Haskoin.Crypto as HC+import qualified Data.Serialize as Bin++-- |Types which contain a pubkey+class Bin.Serialize a => IsPubKey a where+ getPubKey :: a -> HC.PubKeyC++-- |Wrapper for value sender's public key+newtype SendPubKey = MkSendPubKey {+ getSenderPK :: HC.PubKeyC+} deriving (Eq, Show, Bin.Serialize)+instance IsPubKey SendPubKey where+ getPubKey = getSenderPK++-- |Wrapper for value receiver's public key+newtype RecvPubKey = MkRecvPubKey {+ getReceiverPK :: HC.PubKeyC+} deriving (Eq, Show, Bin.Serialize)+instance IsPubKey RecvPubKey where+ getPubKey = getReceiverPK++instance IsPubKey HC.XPubKey where+ getPubKey = HC.xPubKey
src/Data/Bitcoin/PaymentChannel/Internal/Error.hs view
@@ -7,28 +7,24 @@ import GHC.Generics data PayChanError =- SigVerifyFailed |- BadPaymentValue BitcoinAmount |- OutPointMismatch OutPoint |- ChangeAddrMismatch Address |- RedeemScriptMismatch Script |- DustOutput BitcoinAmount |- PartialPaymentBadValue BitcoinAmount |- ClosingPaymentBadValue |- ChannelExpired+ SigVerifyFailed -- ^Signature verification failed+ | BadPaymentValue BitcoinAmount -- ^Payment assigns less value to server than previous payment. Client change value is greater by the specified 'BitcoinAmount'.+ | OutPointMismatch OutPoint -- ^'Network.Haskoin.Transaction.OutPoint' in payment does not match the one in server's state+ | ChangeAddrMismatch Address -- ^Client change 'Network.Haskoin.Crypto.Address' in payment does not match the one in server's state+ | RedeemScriptMismatch Script -- ^redeemScript in payment does not match the one in server's state+ | DustOutput BitcoinAmount -- ^Client change value is less than dust limit (payment transaction would contain a dust output)+ | ClosingPaymentBadValue -- ^The closing payment only changes the payment transaction change address. Sending value is not allowed.+ | ChannelExpired -- ^Channel has expired or is too close to expiration date deriving Generic instance Show PayChanError where- show SigVerifyFailed = "signature verification failed"+ show SigVerifyFailed = "Signature verification failed" show (BadPaymentValue valDiff) = "out-of-order payment (assigns " ++ show valDiff ++ " less value to receiver)" show (OutPointMismatch op) = "unexpected outpoint. expected: " ++ show op show (ChangeAddrMismatch addr) = "unexpected change address. expected: " ++ show addr show (RedeemScriptMismatch scr) = "unexpected redeem script. expected: " ++ cs (serHex scr)- show (PartialPaymentBadValue expVal) = "partial payment change value mismatch." ++- " payment 1/2 with change value " ++ show expVal ++- " was accepted, expecting second payment change value to match that." show (DustOutput limit) = "server dust limit of " ++ show limit ++ " not respected by client change output" show ClosingPaymentBadValue = "payment not of zero value." ++
src/Data/Bitcoin/PaymentChannel/Internal/Refund.hs view
@@ -5,11 +5,13 @@ import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script import Data.Bitcoin.PaymentChannel.Internal.Payment import Data.Bitcoin.PaymentChannel.Internal.Util+import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Fee import qualified Network.Haskoin.Transaction as HT import qualified Network.Haskoin.Crypto as HC import qualified Network.Haskoin.Script as HS + getUnsignedRefundTx :: PaymentChannelState -> BitcoinAmount -> HT.Tx getUnsignedRefundTx st txFee = let@@ -30,22 +32,31 @@ (toWord32 $ pcsLockTime st) -getRefundTxHashForSigning ::- PaymentChannelState+getRefundTxHashForSigning+ :: PaymentChannelState -> BitcoinAmount -- ^Bitcoin transaction fee -> HC.Hash256-getRefundTxHashForSigning pcs@(CPaymentChannelState _ cp _ _ _ _ _) newValueLeft =+getRefundTxHashForSigning pcs@(CPaymentChannelState _ cp _ _ _ _ _) txFee = HS.txSigHash tx (getRedeemScript cp) 0 (HS.SigAll False)- where tx = getUnsignedRefundTx pcs newValueLeft+ where tx = getUnsignedRefundTx pcs txFee -refundTxAddSignature ::+refundTxCreate :: PaymentChannelState- -> BitcoinAmount -- ^Bitcoin tx fee- -> HC.Signature -- ^Signature over 'getUnsignedRefundTx' which verifies against clientPubKey- -> FinalTx-refundTxAddSignature pcs@(CPaymentChannelState _ cp _ _ _ _ _) txFee clientRawSig =+ -> BitcoinAmount -- ^Bitcoin tx fee+ -> (HC.Hash256 -> HC.Signature) -- ^Produces a signature that verifies against clientPubKey+ -> HT.Tx+refundTxCreate pcs@(CPaymentChannelState _ cp _ _ _ _ _) txFee signFunc = let- inputScript = getP2SHInputScript cp $ refundTxScriptSig clientRawSig+ inputScript = getP2SHInputScript cp $ refundTxScriptSig sig+ sig = signFunc $ getRefundTxHashForSigning pcs txFee in replaceScriptInput 0 (serialize inputScript) $ getUnsignedRefundTx pcs txFee+++mkRefundTx :: HasFee fee => PaymentChannelState -> fee -> (HC.Hash256 -> HC.Signature) -> HT.Tx+mkRefundTx pcs txFee signFunc =+ refundTxCreate pcs (absoluteFee (calcTxSize zeroFeeTx) txFee) signFunc+ where+ zeroFeeTx = refundTxCreate pcs (0 :: BitcoinAmount) signFunc+
src/Data/Bitcoin/PaymentChannel/Internal/Serialization.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving, TypeSynonymInstances, FlexibleInstances #-} module Data.Bitcoin.PaymentChannel.Internal.Serialization where @@ -98,9 +97,6 @@ (BinGet.getWord16be >>= BinGet.getByteString . fromIntegral) -deriving instance Bin.Serialize SendPubKey-deriving instance Bin.Serialize RecvPubKey- instance Bin.Serialize PaymentChannelState where put (CPaymentChannelState cfg par fti payConf payCount valLeft sig) = Bin.put cfg >> Bin.put par >> Bin.put fti >> Bin.put payConf >> Bin.put payCount >>@@ -154,12 +150,6 @@ "<FullPayment: payment = " ++ show p ++ " " ++ show (op, script, addr) ++ ">" --- Needed to convert from Scientific-instance Bounded BitcoinAmount where- minBound = BitcoinAmount 0- maxBound = BitcoinAmount $ round $ 21e6 * 1e8-- parseJSONInt :: Scientific -> Parser Integer parseJSONInt s = case toBoundedInteger s of@@ -171,3 +161,14 @@ case toBoundedInteger s of Just w -> return w Nothing -> fail $ "failed to decode JSON number to Word64. data: " ++ show s+++instance Bin.Serialize ReceiverPaymentChannel where+ put (CReceiverPaymentChannel rpc _ ) =+ Bin.put rpc >> Bin.putWord8 0x01+ get = CReceiverPaymentChannel <$> Bin.get <*> return ()++instance Bin.Serialize ReceiverPaymentChannelX where+ put (CReceiverPaymentChannel rpc pki ) =+ Bin.put rpc >> Bin.putWord8 0x02 >> Bin.put pki+ get = CReceiverPaymentChannel <$> Bin.get <*> Bin.get
src/Data/Bitcoin/PaymentChannel/Internal/Settlement.hs view
@@ -1,10 +1,16 @@-module Data.Bitcoin.PaymentChannel.Internal.Settlement where+module Data.Bitcoin.PaymentChannel.Internal.Settlement+(+ createSignedSettlementTx+, signedSettlementTxFromState+)+where import Data.Bitcoin.PaymentChannel.Internal.Types import Data.Bitcoin.PaymentChannel.Internal.Payment import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Util import Data.Bitcoin.PaymentChannel.Internal.Util+import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Fee import qualified Network.Haskoin.Transaction as HT import qualified Network.Haskoin.Crypto as HC@@ -13,10 +19,6 @@ -- |Sign everything, and do not allow additional inputs to be added afterwards. serverSigHash = HS.SigAll False -csFromState :: PaymentChannelState -> ClientSignedPayment-csFromState cs@(CPaymentChannelState _ _ _ _ _ _ clientSig) =- ClientSignedPayment (fromState cs) clientSig- toUnsignedSettlementTx :: ClientSignedPayment -> HC.Address -> BitcoinAmount -> HT.Tx toUnsignedSettlementTx (ClientSignedPayment@@ -33,46 +35,56 @@ in appendOutput adjustedTx recvOut -getSettlementTxHashForSigning ::- ClientSignedPayment+getSettlementTxHashForSigning+ :: ClientSignedPayment -> ChannelParameters -> HC.Address -- ^Receiver destination address- -> BitcoinAmount -- ^Bitcoin transaction fee+ -> BitcoinAmount -- ^Bitcoin transaction fee -> HC.Hash256 getSettlementTxHashForSigning csPayment cp recvAddr txFee = HS.txSigHash tx (getRedeemScript cp) 0 serverSigHash where tx = toUnsignedSettlementTx csPayment recvAddr txFee -settlementSigningTxHashFromState ::- PaymentChannelState- -> HC.Address -- ^Receiver destination address- -> BitcoinAmount -- ^Bitcoin transaction fee- -> HC.Hash256-settlementSigningTxHashFromState cs@(CPaymentChannelState _ cp _ _ _ _ _) =- getSettlementTxHashForSigning (csFromState cs) cp--getSignedSettlementTx ::- ClientSignedPayment+getSignedSettlementTx+ :: ClientSignedPayment -> ChannelParameters -> HC.Address -- ^Receiver/server funds destination address+ -> (HC.Hash256 -> HC.Signature) -- ^ Server/receiver's signing function. Produces a signature which verifies against 'cpReceiverPubKey' -> BitcoinAmount -- ^Bitcoin tx fee- -> HC.Signature -- ^Signature over 'getSettlementTxHashForSigning' which verifies against serverPubKey -> HT.Tx getSignedSettlementTx csPayment@(ClientSignedPayment _ clientSig)- cp recvAddr txFee serverRawSig =+ cp recvAddr signFunc txFee = let+ rawServerSig = signFunc $ getSettlementTxHashForSigning csPayment cp recvAddr txFee unsignedTx = toUnsignedSettlementTx csPayment recvAddr txFee- serverSig = CPaymentSignature serverRawSig serverSigHash+ serverSig = CPaymentSignature rawServerSig serverSigHash inputScript = getP2SHInputScript cp $ paymentTxScriptSig clientSig serverSig in replaceScriptInput 0 (serialize inputScript) unsignedTx -signedSettlementTxFromState ::- PaymentChannelState+createSignedSettlementTx+ :: HasFee fee+ => ClientSignedPayment+ -> ChannelParameters+ -> HC.Address -- ^Receiver/server funds destination address+ -> (HC.Hash256 -> HC.Signature) -- ^Server/receiver's signing function. Produces a signature which verifies against 'cpReceiverPubKey'+ -> fee -- ^Bitcoin tx fee+ -> HT.Tx+createSignedSettlementTx csp cp addr signFunc fee =+ getSignedSettlementTx csp cp addr signFunc (absoluteFee (calcTxSize zeroFeeTx) fee)+ where+ zeroFeeTx = getSignedSettlementTx csp cp addr signFunc (0 :: BitcoinAmount)++signedSettlementTxFromState+ :: HasFee fee+ => PaymentChannelState+ -> HC.Address -- ^Receiver/server funds destination address -> (HC.Hash256 -> HC.Signature) -- ^ Server/receiver's signing function. Produces a signature which verifies against 'cpReceiverPubKey'- -> HC.Address -- ^Receiver/server funds destination address- -> BitcoinAmount -- ^Bitcoin tx fee+ -> fee -- ^Bitcoin tx fee -> HT.Tx-signedSettlementTxFromState cs@(CPaymentChannelState _ cp _ _ _ _ _) signFunc recvAddr txFee =- getSignedSettlementTx (csFromState cs) cp recvAddr txFee serverSig- where serverSig = signFunc (settlementSigningTxHashFromState cs recvAddr txFee)+signedSettlementTxFromState cs@(CPaymentChannelState _ cp _ _ _ _ _) =+ createSignedSettlementTx (cPaymentFromState cs) cp++cPaymentFromState :: PaymentChannelState -> ClientSignedPayment+cPaymentFromState cs@(CPaymentChannelState _ _ _ _ _ _ clientSig) =+ ClientSignedPayment (fromState cs) clientSig
src/Data/Bitcoin/PaymentChannel/Internal/State.hs view
@@ -90,13 +90,25 @@ CPaymentChannelState cfg cp fun pconf (payCount+1) newSenderVal . cpSignature <$> checkDustLimit cfg payment +-- |Create a 'ReceiverPaymentChannelX', which has an associated XPubKey, from a+-- 'ReceiverPaymentChannel'+mkExtendedKeyRPC :: ReceiverPaymentChannel -> HC.XPubKey -> Maybe ReceiverPaymentChannelX+mkExtendedKeyRPC (CReceiverPaymentChannel pcs _) xpk =+ if xPubKey xpk == getPubKey (pcsServerPubKey pcs) then+ Just $ CReceiverPaymentChannel pcs xpk+ else+ Nothing+ checkDustLimit :: Config -> Payment -> Either PayChanError Payment checkDustLimit (Config dustLimit _) payment@(CPayment senderChangeVal _) | senderChangeVal < dustLimit = Left $ DustOutput dustLimit | otherwise = Right payment -isPastLockTimeDate :: UTCTime -> Config -> ChannelParameters -> Bool+isPastLockTimeDate :: UTCTime+ -> Config+ -> ChannelParameters+ -> Bool isPastLockTimeDate currentTime (Config _ settlePeriodHrs) (CChannelParameters _ _ (LockTimeDate expTime)) = currentTime > (settlePeriod `addUTCTime` expTime) where settlePeriod = -1 * fromIntegral (toSeconds settlePeriodHrs) :: NominalDiffTime
src/Data/Bitcoin/PaymentChannel/Internal/Types.hs view
@@ -1,20 +1,20 @@ {-# LANGUAGE DeriveGeneric, DataKinds #-}-+{-# LANGUAGE GeneralizedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances #-} module Data.Bitcoin.PaymentChannel.Internal.Types ( module Data.Bitcoin.PaymentChannel.Internal.Types , module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount , module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime+ , module Data.Bitcoin.PaymentChannel.Internal.Crypto.PubKey , module Network.Haskoin.Transaction , module Network.Haskoin.Crypto , module Network.Haskoin.Script-)--where+) where import Data.Bitcoin.PaymentChannel.Internal.Util import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime+import Data.Bitcoin.PaymentChannel.Internal.Crypto.PubKey import Network.Haskoin.Transaction import Network.Haskoin.Crypto@@ -27,14 +27,6 @@ import qualified Data.Tagged as Tag -defaultConfig = Config defaultDustLimit defaultSettlementPeriod--defaultDustLimit = 700 :: BitcoinAmount-defaultSettlementPeriod = 10 :: Hour--newtype UnsignedPaymentTx = CUnsignedPaymentTx { unsignedTx :: HT.Tx } deriving Show-type FinalTx = HT.Tx- -- |Shared state object used by both value sender and value receiver. data PaymentChannelState = CPaymentChannelState { -- |Holds various optional configuration options@@ -63,8 +55,8 @@ -- the channel data FundingTxInfo = CFundingTxInfo { ftiHash :: HT.TxHash, -- ^ Hash of funding transaction.- ftiOutIndex :: Word32, -- ^ Index/"vout" of funding output- ftiOutValue :: BitcoinAmount -- ^ Value of funding output (channel max value)+ ftiOutIndex :: Word32, -- ^ Index/"vout" of funding output (zero-based index of funding output within list of transaction outputs)+ ftiOutValue :: BitcoinAmount -- ^ Value of funding output (channel max value). } deriving (Eq, Show, Typeable) -- |Holds information about how to construct the payment transaction@@ -87,7 +79,7 @@ data Payment = CPayment { -- |Channel value remaining ('pcsValueLeft' of the state from which this payment was made) cpClientChange :: BitcoinAmount- -- |Payment signature+ -- |Get payment signature from payment , cpSignature :: PaymentSignature } deriving (Eq, Typeable) @@ -96,8 +88,9 @@ fpPayment :: Payment -- |The payment transaction redeems this outpoint , fpOutPoint :: HT.OutPoint+ -- |Using this redeemScript , fpRedeemScript :: HS.Script- -- |Client change output address in the payment tx+ -- |Paying this amount to the client/sender change output , fpChangeAddr :: HC.Address } deriving (Eq, Typeable) @@ -111,23 +104,41 @@ -- |Wraps a Network.Haskoin.Script.Script newtype ChanScript = ChanScript { getScript :: HS.Script } deriving (Eq, Show) --- Never confuse sender/receiver pubkey-newtype SendPubKey = MkSendPubKey {- getSenderPK :: HC.PubKey-} deriving (Eq, Show)+type PayChanState = PaymentChannelState+type ChanParams = ChannelParameters -newtype RecvPubKey = MkRecvPubKey {- getReceiverPK :: HC.PubKey-} deriving (Eq, Show)+-- |ReceiverPaymentChannel without public key metadata+type ReceiverPaymentChannel = ReceiverPaymentChannelI ()+-- |ReceiverPaymentChannel with BIP32 , "extended" public key metadata+type ReceiverPaymentChannelX = ReceiverPaymentChannelI HC.XPubKey -class IsPubKey a where- getPubKey :: a -> HC.PubKey+-- |State object for the value receiver. pkInfo holds optional, extra+-- data associated with the receiver public key+data ReceiverPaymentChannelI pkInfo = CReceiverPaymentChannel {+ -- |Internal state object+ rpcState :: PaymentChannelState+ , rpcPubKeyInfo :: pkInfo+} deriving (Eq, Typeable) -instance IsPubKey SendPubKey where- getPubKey = getSenderPK-instance IsPubKey RecvPubKey where- getPubKey = getReceiverPK +instance Show ReceiverPaymentChannel where+ show (CReceiverPaymentChannel s _) =+ "<ReceiverPaymentChannel:\n\t" ++ show s ++ ">"++instance Show ReceiverPaymentChannelX where+ show (CReceiverPaymentChannel s _) =+ "<ReceiverPaymentChannelX:\n\t" ++ show s ++ ">"+++rpcGetXPub = rpcPubKeyInfo+ type Hour = Tag.Tagged "Hour" Word32 toSeconds :: Hour -> Integer toSeconds = fromIntegral . (* 3600) . Tag.unTagged+++-- Defaults+defaultConfig = Config defaultDustLimit defaultSettlementPeriod++defaultDustLimit = 700 :: BitcoinAmount+defaultSettlementPeriod = 10 :: Hour
src/Data/Bitcoin/PaymentChannel/Internal/Util.hs view
@@ -24,9 +24,13 @@ import qualified Data.Text as T import qualified Data.Aeson.Types as JSON import qualified Network.Haskoin.Crypto as HC+import qualified Network.Haskoin.Transaction as HT import Data.String.Conversions (cs) import Data.Text.Encoding (decodeUtf8, encodeUtf8) ++calcTxSize :: HT.Tx -> Word+calcTxSize = fromIntegral . B.length . Bin.encode mapLeft f = either (Left . f) Right mapRight f = either Left (Right . f)
+ src/Data/Bitcoin/PaymentChannel/Test.hs view
@@ -0,0 +1,113 @@+module Data.Bitcoin.PaymentChannel.Test+(+ module Data.Bitcoin.PaymentChannel.Test+ , module Data.Bitcoin.PaymentChannel+ , module Data.Bitcoin.PaymentChannel.Types+ , module Data.Bitcoin.PaymentChannel.Internal.Types+ , module Data.Bitcoin.PaymentChannel.Internal.State+)++where++import Data.Bitcoin.PaymentChannel+import Data.Bitcoin.PaymentChannel.Types+import Data.Bitcoin.PaymentChannel.Internal.Types+import Data.Bitcoin.PaymentChannel.Util+import Data.Bitcoin.PaymentChannel.Internal.State hiding (channelIsExhausted, channelValueLeft)++import qualified Network.Haskoin.Crypto as HC+import Network.Haskoin.Test+import Data.Time.Clock (UTCTime(..))+import Data.Time.Calendar (Day(..))++import Test.QuickCheck+++-- TODO: We don't bother testing expiration time for now+nowishTimestamp :: UTCTime+nowishTimestamp = UTCTime (ModifiedJulianDay 57683) 0 -- 2016-10-22++mIN_CHANNEL_SIZE :: BitcoinAmount+mIN_CHANNEL_SIZE = cDustLimit defaultConfig * 2+++data ArbChannelPair = ArbChannelPair+ SenderPaymentChannel ReceiverPaymentChannel [BitcoinAmount] [BitcoinAmount] (HC.Hash256 -> HC.Signature)++instance Show ArbChannelPair where+ show (ArbChannelPair spc rpc _ _ _) =+ "SendState: " ++ show spc ++ "\n" +++ "RecvState: " ++ show rpc++doPayment :: ArbChannelPair -> BitcoinAmount -> ArbChannelPair+doPayment (ArbChannelPair spc rpc sendList recvList f) amount =+ let+ (amountSent, pmn, newSpc) = sendPayment spc amount+ eitherRpc = recvPayment nowishTimestamp rpc pmn+ in+ case eitherRpc of+ Left e -> error (show e)+ Right (recvAmount, newRpc) ->+ ArbChannelPair newSpc newRpc+ (amountSent : sendList)+ (recvAmount : recvList)+ f++instance Arbitrary ArbChannelPair where+ arbitrary = fmap fst mkChanPair++instance Arbitrary PaymentChannelState where+ arbitrary = fmap getPCS mkChanPair+ where getPCS (ArbChannelPair _ rpc _ _ _ , _) = rpcState rpc++instance Arbitrary ChanScript where+ arbitrary = ChanScript . getRedeemScript <$> arbitrary++instance Arbitrary FullPayment where+ arbitrary = fmap snd mkChanPair++instance Arbitrary ChannelParameters where+ arbitrary = fmap fst mkChanParams++instance Arbitrary FundingTxInfo where+ arbitrary = do+ ArbitraryTxHash h <- arbitrary+ i <- arbitrary+ amt <- fmap fromIntegral+ (choose (fromIntegral mIN_CHANNEL_SIZE, round $ 21e6 * 1e8 :: Integer))+ :: Gen BitcoinAmount+ return $ CFundingTxInfo h i amt++instance Arbitrary BitcoinAmount where+ arbitrary = fromIntegral <$> choose (0, round $ 21e6 * 1e8 :: Integer)+++mkChanParams :: Gen (ChannelParameters, (HC.PrvKeyC, HC.PrvKeyC))+mkChanParams = do+ -- sender key pair+ ArbitraryPubKeyC sendPriv sendPK <- arbitrary+ -- receiver key pair+ ArbitraryPubKeyC recvPriv recvPK <- arbitrary+ -- TODO: We use an expiration date far off into the future for now+ let lockTime = parseBitcoinLocktime 2524651200+ return (CChannelParameters+ (MkSendPubKey sendPK) (MkRecvPubKey recvPK) lockTime,+ (sendPriv, recvPriv))++mkChanPair :: Gen (ArbChannelPair, FullPayment)+mkChanPair = do+ (cp, (sendPriv, recvPriv)) <- mkChanParams+ fti <- arbitrary+ -- value of first payment+ initPayAmount <- arbitrary+ -- create states+ let (initPayActualAmount,paymnt,sendChan) = channelWithInitialPaymentOf defaultConfig+ cp fti (flip HC.signMsg sendPriv) (getFundingAddress cp) initPayAmount+ let eitherRecvChan = channelFromInitialPayment nowishTimestamp defaultConfig cp fti paymnt+ case eitherRecvChan of+ Left e -> error (show e)+ Right (initRecvAmount,recvChan) -> return+ (ArbChannelPair+ sendChan recvChan [initPayActualAmount] [initRecvAmount]+ (flip HC.signMsg recvPriv),+ paymnt)
src/Data/Bitcoin/PaymentChannel/Types.hs view
@@ -8,24 +8,41 @@ -} -{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE GeneralizedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances #-} module Data.Bitcoin.PaymentChannel.Types (- PaymentChannel(..),- SenderPaymentChannel(..),- ReceiverPaymentChannel(..),+ -- *Interface+ PaymentChannel(..), PayChan,++ -- *State+ SenderPaymentChannel(..), SendPayChan,+ ReceiverPaymentChannel, ReceiverPaymentChannelI, RecvPayChan,+ ReceiverPaymentChannelX, RecvPayChanX,++ -- *Config Config(..),defaultConfig,+ FundingTxInfo(..),+ ChannelParameters(..), ChanParams,++ -- *Payment Payment,cpSignature, FullPayment(..),- FundingTxInfo(..),- ChannelParameters(..),- PayChanError(..),- PaymentChannelState,- SendPubKey(..),RecvPubKey(..),IsPubKey(..),- BitcoinAmount,- BitcoinLockTime(..), fromDate,- usesBlockHeight+ -- **Error+ PayChanError(..)++ -- *Bitcoin+, BitcoinAmount+, BitcoinLockTime(..)+, module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Fee++ -- *Crypto+, SendPubKey(..),RecvPubKey(..),IsPubKey(..),++ -- *Util+ S.mkExtendedKeyRPC, rpcGetXPub, fromDate, usesBlockHeight++-- PaymentChannelState, ) where @@ -35,6 +52,7 @@ import qualified Data.Bitcoin.PaymentChannel.Internal.State as S import qualified Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script as Script import Data.Bitcoin.PaymentChannel.Internal.Error (PayChanError(..))+import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Fee import qualified Data.Serialize as Bin import qualified Network.Haskoin.Crypto as HC@@ -83,22 +101,17 @@ data SenderPaymentChannel = CSenderPaymentChannel { -- |Internal state object spcState :: PaymentChannelState,+ -- |Payment-signing function spcSignFunc :: HC.Hash256 -> HC.Signature } --- |State object for the value receiver-newtype ReceiverPaymentChannel = CReceiverPaymentChannel {- -- |Internal state object- rpcState :: PaymentChannelState-} deriving (Eq, Bin.Serialize)- instance PaymentChannel SenderPaymentChannel where valueToMe = channelValueLeft getChannelState = spcState _setChannelState spc s = spc { spcState = s } instance PaymentChannel ReceiverPaymentChannel where- valueToMe rpc@(CReceiverPaymentChannel s) =+ valueToMe rpc@(CReceiverPaymentChannel s _) = S.pcsChannelTotalValue s - channelValueLeft rpc getChannelState = rpcState _setChannelState rpc s = rpc { rpcState = s }@@ -107,6 +120,13 @@ show (CSenderPaymentChannel s _) = "<SenderPaymentChannel:\n\t" ++ show s ++ ">" -instance Show ReceiverPaymentChannel where- show (CReceiverPaymentChannel s) =- "<ReceiverPaymentChannel:\n\t" ++ show s ++ ">"++-- |Short-hand+type SendPayChan = SenderPaymentChannel+-- |Short-hand+type RecvPayChan = ReceiverPaymentChannel+-- |Short-hand+type RecvPayChanX = ReceiverPaymentChannelX+-- |Short-hand+class PaymentChannel a => PayChan a+
src/Data/Bitcoin/PaymentChannel/Util.hs view
@@ -23,7 +23,7 @@ where import Data.Bitcoin.PaymentChannel.Internal.Types- (PaymentChannelState(..),+ (PaymentChannelState(..), ReceiverPaymentChannelI(..), Payment(..), PaymentSignature(..)) import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script (getP2SHFundingAddress, getRedeemScript)@@ -55,9 +55,9 @@ -- |Update internal state without signature verification. -- Useful for database-type services where a logic layer has already -- verified the signature, and it just needs to be stored.-unsafeUpdateRecvState :: ReceiverPaymentChannel -> Payment -> ReceiverPaymentChannel-unsafeUpdateRecvState (CReceiverPaymentChannel s) (CPayment val sig) =- CReceiverPaymentChannel $ s { pcsClientChangeVal = val, pcsPaymentSignature = sig}+unsafeUpdateRecvState :: ReceiverPaymentChannelI a -> Payment -> ReceiverPaymentChannelI a+unsafeUpdateRecvState (CReceiverPaymentChannel s pki) (CPayment val sig) =+ CReceiverPaymentChannel ( s { pcsClientChangeVal = val, pcsPaymentSignature = sig} ) pki fpGetSig :: FullPayment -> HC.Signature fpGetSig = psSig . cpSignature . fpPayment
test/Main.hs view
@@ -1,36 +1,19 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}- module Main where --import Data.Bitcoin.PaymentChannel-import Data.Bitcoin.PaymentChannel.Types+import Data.Bitcoin.PaymentChannel.Test import Data.Bitcoin.PaymentChannel.Util-import qualified Data.Bitcoin.PaymentChannel.Internal.State as S import qualified Network.Haskoin.Transaction as HT import qualified Network.Haskoin.Crypto as HC-import qualified Network.Haskoin.Script as HS-import Network.Haskoin.Test import qualified Data.Aeson as JSON import qualified Data.Serialize as Bin import Data.Typeable -import Data.Time.Clock (UTCTime(..))-import Data.Time.Calendar (Day(..))--import Test.QuickCheck import Test.Framework (Test, testGroup, defaultMain) import Test.Framework.Providers.QuickCheck2 (testProperty) --- |We don't bother testing timestamps for now-nowishTimestamp :: UTCTime-nowishTimestamp = UTCTime (ModifiedJulianDay 57683) 0 -- 2016-10-22 -mIN_CHANNEL_SIZE :: BitcoinAmount-mIN_CHANNEL_SIZE = cDustLimit defaultConfig * 2- testAddrTestnet :: HC.Address testAddrTestnet = "2N414xMNQaiaHCT5D7JamPz7hJEc9RG7469" testAddrLivenet :: HC.Address@@ -53,33 +36,35 @@ testPaymentSession checkRecvSendAmount ] , testGroup "Serialization"- [ testProperty "FullPayment JSON" (jsonSerDeser :: FullPayment -> Bool)- , testProperty "FullPayment Binary" testPaymentBin- -- Fails: https://github.com/haskoin/haskoin/issues/287- -- , testProperty "ChanScript ser/deser" testScriptBin+ [ testGroup "JSON"+ [ testProperty "FullPayment"+ (jsonSerDeser :: FullPayment -> Bool)+ ]+ , testGroup "Binary"+ [ testProperty "FullPayment"+ (binSerDeser :: FullPayment -> Bool)+ , testProperty "PaymentChannelState"+ (binSerDeser :: PaymentChannelState -> Bool)+ , testProperty "ChanScript"+ (binSerDeser :: ChanScript -> Bool)+ ] ] ] -testPaymentJSON :: FullPayment -> Bool-testPaymentJSON = jsonSerDeser-testPaymentBin :: FullPayment -> Bool-testPaymentBin = binSerDeser-testScriptBin :: ChanScript -> Bool -- Fails: https://github.com/haskoin/haskoin/issues/287-testScriptBin = binSerDeser checkSenderValue :: (ArbChannelPair, [BitcoinAmount]) -> Bool checkSenderValue (ArbChannelPair _ recvChan amountSent _ recvSignFunc, _) = do- let settleTx = getSettlementBitcoinTx recvChan recvSignFunc testAddrLivenet 0+ let settleTx = getSettlementBitcoinTx recvChan testAddrLivenet recvSignFunc (0 :: BitcoinAmount) let clientChangeAmount = HT.outValue . head . HT.txOut $ settleTx -- Check that the client change amount in the settlement transaction equals the -- channel funding amount minus the sum of all payment amounts.- let fundAmountMinusPaySum = S.pcsChannelTotalValue (getChannelState recvChan) -+ let fundAmountMinusPaySum = pcsChannelTotalValue (getChannelState recvChan) - fromIntegral (sum amountSent) fromIntegral clientChangeAmount == fundAmountMinusPaySum checkReceiverValue :: (ArbChannelPair, [BitcoinAmount]) -> Bool checkReceiverValue (ArbChannelPair _ recvChan amountSent _ recvSignFunc, _) = do- let settleTx = getSettlementBitcoinTx recvChan recvSignFunc testAddrLivenet 0+ let settleTx = getSettlementBitcoinTx recvChan testAddrLivenet recvSignFunc (0 :: BitcoinAmount) let receiverAmount = HT.outValue (HT.txOut settleTx !! 1) -- Check receiver amount in settlement transaction with zero fee equals sum -- of all payments.@@ -129,82 +114,3 @@ True ---data ArbChannelPair = ArbChannelPair- SenderPaymentChannel ReceiverPaymentChannel [BitcoinAmount] [BitcoinAmount] (HC.Hash256 -> HC.Signature)--instance Show ArbChannelPair where- show (ArbChannelPair spc rpc _ _ _) =- "SendState: " ++ show spc ++ "\n" ++- "RecvState: " ++ show rpc--doPayment :: ArbChannelPair -> BitcoinAmount -> ArbChannelPair-doPayment (ArbChannelPair spc rpc sendList recvList f) amount =- let- (amountSent, pmn, newSpc) = sendPayment spc amount- eitherRpc = recvPayment nowishTimestamp rpc pmn- in- case eitherRpc of- Left e -> error (show e)- Right (recvAmount, newRpc) ->- ArbChannelPair newSpc newRpc- (amountSent : sendList)- (recvAmount : recvList)- f--newtype ChanScript = ChanScript HS.Script deriving (Eq,Show,Bin.Serialize)--instance Arbitrary ArbChannelPair where- arbitrary = fmap fst mkChanPair--instance Arbitrary FullPayment where- arbitrary = fmap snd mkChanPair--instance Arbitrary ChannelParameters where- arbitrary = fmap fst mkChanParams--instance Arbitrary ChanScript where- arbitrary = ChanScript . getRedeemScript <$> arbitrary--mkChanParams :: Gen (ChannelParameters, (HC.PrvKey, HC.PrvKey))-mkChanParams = do- -- sender key pair- ArbitraryPubKey sendPriv sendPK <- arbitrary- -- receiver key pair- ArbitraryPubKey recvPriv recvPK <- arbitrary- -- TODO: We use an expiration date far off into the future for now- let lockTime = parseBitcoinLocktime 2524651200- return (CChannelParameters- (MkSendPubKey sendPK) (MkRecvPubKey recvPK) lockTime,- (sendPriv, recvPriv))--mkChanPair :: Gen (ArbChannelPair, FullPayment)-mkChanPair = do- (cp, (sendPriv, recvPriv)) <- mkChanParams- fti <- arbitrary- -- value of first payment- initPayAmount <- arbitrary- -- create states- let (initPayActualAmount,paymnt,sendChan) = channelWithInitialPaymentOf defaultConfig- cp fti (flip HC.signMsg sendPriv) (getFundingAddress cp) initPayAmount- let eitherRecvChan = channelFromInitialPayment nowishTimestamp defaultConfig cp fti paymnt- case eitherRecvChan of- Left e -> error (show e)- Right (initRecvAmount,recvChan) -> return- (ArbChannelPair- sendChan recvChan [initPayActualAmount] [initRecvAmount]- (flip HC.signMsg recvPriv),- paymnt)--instance Arbitrary FundingTxInfo where- arbitrary = do- ArbitraryTxHash h <- arbitrary- i <- arbitrary- amt <- fmap fromIntegral- (choose (fromIntegral mIN_CHANNEL_SIZE, round $ 21e6 * 1e8 :: Integer))- :: Gen BitcoinAmount- return $ CFundingTxInfo h i amt--instance Arbitrary BitcoinAmount where- arbitrary = fromIntegral <$> choose (0, round $ 21e6 * 1e8 :: Integer)